Create a React project from scratch, with and Redux Toolkit and Typescript
Setup
Prerequisites:
- node
- yarn
mkdir react-rtk-app
cd react-rtk-app
Generate a default package.json file with yarn:
yarn init -y
Install React, Redux Toolkit, Typescript and Webpack:
yarn add react \
react-dom \
react-router-dom \
@reduxjs/toolkit \
react-redux
yarn add --dev @types/react \
@types/react-dom \
@types/react-router-dom \
@types/react-redux \
ts-loader \
css-loader \
html-webpack-plugin \
sass \
sass-loader \
style-loader \
typescript \
webpack \
webpack-cli \
webpack-dev-server
Open package.json and add:
react-rtk-app/package.json ....
},
"scripts": {
"clean": "rm -rf dist/*",
"build": "webpack",
"dev": "webpack serve"
}
Create a file tsconfig.json:
react-rtk-app/tsconfig.json{
"compilerOptions": {
"incremental": true,
"target": "es5",
"module": "commonjs",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"jsx": "react",
"sourceMap": true,
"outDir": "./dist/",
"rootDir": ".",
"removeComments": true,
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true
},
"include": [
"./src"
],
"exclude": [
"./node_modules",
"./build",
"./dist"
]
}
Create a file webpack.config.js and add:
react-rtk-app/webpack.config.jsconst path = require("path");
const app_dir = __dirname + '/src';
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HTMLWebpackPluginConfig = new HtmlWebpackPlugin({
template: app_dir + '/index.html',
filename: 'index.html',
inject: 'body'
});
const config = {
mode: 'development',
entry: app_dir + '/app.tsx',
output: {
path: __dirname + '/dist',
filename: 'app.js',
publicPath: '/'
},
module: {
rules: [{
test: /\.s?css$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
}, {
test: /\.tsx?$/,
loader: "ts-loader",
exclude: /(node_modules|bower_components)/
},
{
test: /\.(woff|woff2|ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
exclude: [/node_modules/],
loader: "file-loader"
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
exclude: [/node_modules/],
loader: "file-loader"
},
{
test: /\.(pdf)$/i,
exclude: [/node_modules/],
loader: "file-loader",
options: {
name: '[name].[ext]',
},
},
]
},
plugins: [HTMLWebpackPluginConfig],
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx"]
},
optimization: {
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
},
devServer: {
port: 8080,
// open: true,
hot: true,
historyApiFallback: true,
},
};
module.exports = config;
A simple Note taking App
Create a folder named src/notes (in the project's folder):
mkdir -p src/notes
cd src
Data Model
Create a file named notes.model.ts (in src/notes):
react-rtk-app/src/notes/notes.model.tsexport interface INoteData {
id: string
title: string
content: string
}
Redux Setup
Add a Redux Toolkt Slice in notes/notes.slice.ts:
react-rtk-app/src/notes/notes.slice.tsimport { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { INoteData } from "./notes.model"
const initialState: INoteData[] = [{
id: "note" + (Math.random() * 1000000),
title: "Test note",
content: "test"
}]
const notesSlice = createSlice({
name: 'Notes',
initialState,
reducers: {
addNote(state, action: PayloadAction<INoteData>) {
state.push(action.payload)
},
updateNote(state, action: PayloadAction<INoteData>) {
let atIndex = state.findIndex(snippet => snippet.id === action.payload.id)
state[atIndex] = action.payload
},
removeNote(state, action: PayloadAction<INoteData>) {
let atIndex = state.findIndex(snippet => snippet.id === action.payload.id)
state.splice(atIndex, 1)
},
}
})
export const { addNote, updateNote, removeNote } = notesSlice.actions
export default notesSlice.reducer
Create the Root Reducer in root.reducer.ts, in the src/ folder:
react-rtk-app/src/root.reducer.tsimport { combineReducers } from '@reduxjs/toolkit'
import Notes from './notes/notes.slice'
const rootReducer = combineReducers({
//...
Notes
})
export type RootState = ReturnType<typeof rootReducer>
export default rootReducer
Create the Store in src/store.ts:
react-rtk-app/src/store.tsimport { configureStore, Action } from '@reduxjs/toolkit'
import rootReducer, { RootState } from './root.reducer'
const store = configureStore({
reducer: rootReducer,
})
export default store
UI Components & Views
Add the main view - a list with all the notes - in notes/notes.tsx:
react-rtk-app/src/notes/notes.tsximport React from "react"
import { useSelector } from "react-redux"
import { Link, useNavigate } from "react-router-dom"
import { RootState } from "../root.reducer"
import { INoteData } from "./notes.model"
import './notes.scss'
interface INotesProps {
}
const Notes = (props: INotesProps) => {
const navigate = useNavigate()
const allNotes: INoteData[] = useSelector((state: RootState) => state.Notes)
const onNewNote = () => {
navigate("/new")
}
return (
<div className="all-notes">
<div className="container">
<div className="menu">
<button onClick={onNewNote}>+ New Note</button>
</div>
</div>
<div className="note-list">
{allNotes.map((note) => <Link key={note.id} to={"/note/" + note.id} className="note-link">{note.title}</Link>)}
</div>
</div>
)
}
export default Notes
and notes/notes.scss:
react-rtk-app/src/notes/notes.scss.all-notes {
width: 100%; height: 100%;
display: flex; flex-direction: column;
align-items: center;
}
.menu {
width: 600px;
}
.note-list {
width: 600px; height: 100%;
display: flex; flex-direction: column;
align-content: flex-start;
margin-top: 20px;
}
.note-link {
margin-top: 10px;
}
Create the note editor in notes/editor.tsx:
react-rtk-app/src/notes/editor.tsximport React, { useEffect, useState } from "react"
import { useDispatch, useSelector } from "react-redux";
import { Link, useNavigate, useParams } from "react-router-dom";
import { RootState } from "../root.reducer";
import { addNote, updateNote, removeNote } from "./notes.slice";
import { INoteData } from "./notes.model";
import './editor.scss'
interface RouteParams {
noteId?: string
}
const NoteEditor = () => {
const params: RouteParams = useParams()
const dispatch = useDispatch()
const navigate = useNavigate()
const allNotes: INoteData[] = useSelector((state: RootState) => state.Notes)
const [noteData, setNoteData] = useState({
id: 'doc' + Math.floor(Math.random() * 1000000000),
title: "New Note",
content: ''
} as INoteData)
useEffect(() => {
if (params.noteId) {
let tmpNotes = allNotes.filter(note => note.id === params.noteId)
if (tmpNotes.length > 0) {
setNoteData(tmpNotes[0])
}
} else {
dispatch(addNote(noteData)) // if nothing found, add the default note
}
}, []);
const onTitleChange = (e: any) => {
const newTitle = e.target.value as string
if (newTitle.trim().length === 0) {
// TODO -> title error - cannot be empty
}
const payload = {
...noteData,
title: newTitle
}
setNoteData(payload)
dispatch(updateNote(payload))
}
const onContentChange = (e: any) => {
const newContent = e.target.value as string
const payload = {
...noteData,
content: newContent
}
setNoteData(payload)
dispatch(updateNote(payload))
}
const onRemoveNote = () => {
const ans = confirm("Are you sure you want to delete this note?")
if (ans) {
dispatch(removeNote(noteData))
navigate('/')
}
}
return (
<div className="note-editor">
<div className="editor-toolbar">
<div className="menu">
<div className="start-menu">
<button onClick={() => navigate('/')}>← All Notes</button>
</div>
<div className="end-menu">
<button onClick={onRemoveNote} className="important">Delete</button>
</div>
</div>
</div>
<div className="note-title">
<input
type="text"
value={noteData.title}
onChange={onTitleChange}
className="doc-title-input"
/>
</div>
<div className="note-content">
<textarea
value={noteData.content}
onChange={onContentChange}
></textarea>
</div>
</div>
)
}
export default NoteEditor
and notes/editor.scss:
react-rtk-app/src/notes/editor.scss.note-editor {
width: 100%; height: 100%;
display: flex; flex-direction: column;
align-items: center;
box-sizing: border-box;
}
.menu {
width: 600px;
display: flex; flex-direction: row;
justify-content: space-between;
}
.note-title {
width: 600px;
margin-top: 10px;
input {
width: 600px;
padding: 2px;
}
}
.note-content {
width: 600px; height: 100%;
textarea {
width: 600px; height: 300px;
margin-top: 20px;
}
}
Then, in the src/ folder create the root (app) component, in app.tsx:
react-rtk-app/src/app.tsximport * as React from 'react';
import * as ReactDOM from 'react-dom';
import {
BrowserRouter as Router,
Routes,
Route
} from "react-router-dom";
import { Provider } from 'react-redux';
import store from './store';
import NoteEditor from './notes/editor';
import Notes from './notes/notes';
import './app.scss'
const App = () => {
return (
<Provider store={store}>
<Router>
<Routes>
<Route path="/" element={<Notes />} />
<Route path="/new" element={<NoteEditor />} />
<Route path="/note/:noteId" element={<NoteEditor />} />
</Routes>
</Router>
</Provider>
)
}
ReactDOM.render(
<App />,
document.getElementById('app') as HTMLElement
);
and app.scss:
react-rtk-app/src/app.scsshtml, body {
width: 100%; height: 100%; margin: 0; padding: 0;
}
* {
box-sizing: border-box;
}
#app {
width: 100%; height: 100%; margin: 0; padding: 0;
}
Finally, add the index.html:
react-rtk-app/src/index.html<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React TypeScript</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
You can find the source code here