Create a React project from scratch, with and Redux Toolkit and Typescript

Setup

Prerequisites:

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.js
const 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.ts
export 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.ts
import { 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.ts
import { 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.ts
import { 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.tsx
import 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.tsx
import 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('/')}>&larr; 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.tsx
import * 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.scss
html, 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>

Editor View

You can find the source code here