React.js
React.js, often referred to simply as React, is a JavaScript library for building user interfaces. When you build a user interface with React, you will first break it apart into pieces called components. Then, you will describe the different visual states for each of your components. Finally, you will connect your components together so that the data flows through them.
React-Redux
React Redux is a library that provides official bindings to connect React components with Redux, a predictable state container for JavaScript applications.
MUI
MUI (formerly known as Material-UI) is a popular React component library that implements Googleโs Material Design.
Set Up Your React App
npx create-react-app todo-app
Navigate to project directory
cd todo-app
Install react-redux
npx install @redux/toolkit react-redux
Install MUI
npm install @mui/material @emotion/react @emotion/styled
Now you can start the development server by running the following command:
npm start
Files
index.js
we will create provider for redux store.
\\todo-app\src\index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store/store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<React.StrictMode>
<App />
</React.StrictMode>
</Provider>
);
App.js
\todo-app\src\App.js
import "./App.css";
import ButtonAddTodo from "./component/ButtonAddTodo";
import InputTodo from "./component/InputTodo";
import ListTodo from "./component/ListTodo";
import { useDispatch, useSelector } from "react-redux";
import { addTask } from "./store/taskStore";
import { useState } from "react";
import { Container, Snackbar, Stack, Typography } from "@mui/material";
function App() {
const [inputText, setInputText] = useState("");
const [showSnack, setShowSnack] = useState(false);
const todos = useSelector((state) => state);
const dispatch = useDispatch();
return (
<Container maxWidth={"sm"}>
<Typography
variant="subtitle1"
style={{
textAlign: "center",
margin: 24,
font: "status-bar",
fontSize: 23,
color: "slateblue"
}}
>
Todo App
</Typography>
<Stack direction={"row"} spacing={2} style={{ marginBottom: 32 }}>
<InputTodo inputText={inputText} setInputText={setInputText} />
<ButtonAddTodo
handleAdd={() => {
if (inputText.length === 0) {
setShowSnack(true);
return;
}
dispatch(addTask({ title: inputText }));
setInputText("");
}}
/>
</Stack>
<ListTodo
todos={todos.slice().reverse()}
setInputText={setInputText}
inputText={inputText}
/>
<Snackbar
open={showSnack}
autoHideDuration={2000}
onClose={() => {
setShowSnack(false);
}}
message="Failed to add, please add something to add todo"
/>
</Container>
);
}
export default App;
store.js
we create store and add reducer taskReducer.
\\todo-app\src\store\store.js
import { configureStore } from "@reduxjs/toolkit";
import { taskReducer } from "./taskStore";
export default configureStore({ reducer: taskReducer });
taskStore.js
we createSlice ie. immutable state(shallowcopy) just like slice does which will be updated by action, passing the initial state and name the slice "task".
import { createSlice } from "@reduxjs/toolkit";
const initialState = [];
const taskSlice = createSlice({
name: "task",
initialState,
reducers: {
addTask: (state, action) => {
state.push({
id: state.length + 1,
title: action.payload.title,
completed: false
});
},
markTaskToggle: (state, action) => {
const todo = state.find((item) => item.id === action.payload.id);
if (todo) {
todo.completed = !todo.completed; //works as still wrapped in proxy.
}
},
deleteTask: (state, action) => {
// console.log("id: " + action.payload.id);
// state.splice(action.payload.id - 1, 1);
return state.filter((item) => item.id !== action.payload.id);
},
isEditable: (state, action) => {
const todo = state.find((item) => item.id === action.payload.id);
if (todo) {
todo.editable = action.payload.editable; //works as still wrapped in proxy.
}
},
saveTask: (state, action) => {
const todo = state.find((item) => item.id === action.payload.id);
if (todo) {
todo.title = action.payload.title; //works as still wrapped in proxy.
}
}
}
});
export const { addTask, markTaskToggle, deleteTask, isEditable, saveTask } =
taskSlice.actions;
export const taskReducer = taskSlice.reducer;
ButtonAddTodo.js
\\todo-app\src\component\ButtonAddTodo.js
import { Button } from "@mui/material";
import { memo } from "react";
const ButtonAddTodo = ({ handleAdd }) => {
return (
<>
<Button variant="outlined" onClick={handleAdd}>
Add
</Button>
</>
);
};
export default memo(ButtonAddTodo);
InputTodo.js
\todo-app\src\component\InputTodo.js
import { useDispatch } from "react-redux";
import { addTask } from "../store/taskStore";
import { TextField } from "@mui/material";
const InputTodo = ({ inputText, setInputText }) => {
const dispatch = useDispatch();
const handleInput = (e) => {
setInputText(e.target.value);
};
return (
<>
<TextField
variant="outlined"
aria-label="Todo"
style={{ flex: 1 }}
value={inputText}
onChange={handleInput}
placeholder="Todo.."
onKeyUp={(e) => {
if (e.key === "Enter") {
dispatch(addTask({ title: inputText }));
setInputText("");
}
}}
/>
</>
);
};
export default InputTodo;
ListTodo.js
\todo-app\src\component\ListTodo.js
import { useDispatch } from "react-redux";
import {
deleteTask,
markTaskToggle,
isEditable,
saveTask
} from "../store/taskStore";
import { Button, Checkbox, Stack, TextField, Typography } from "@mui/material";
import { useRef, useState } from "react";
const ListTodos = ({ todos }) => {
const dispatch = useDispatch();
const [editText, setEditText] = useState("");
const editEnabledIdRef = useRef(null);
function handleSave(item) {
dispatch(saveTask({ id: item.id, title: editText }));
dispatch(isEditable({ id: item.id, isEditable: false }));
}
return (
<>
{todos.map((item) => {
return (
<Stack direction={"row"} key={item.id} spacing={2} padding={1}>
<Checkbox
aria-label={"label " + item.title}
checked={item.completed}
onChange={() => {
console.log("item id " + item.id);
dispatch(markTaskToggle({ id: item.id }));
}}
/>
{item.editable ? (
<TextField
onKeyUp={(e) => {
if (e.key === "Enter") {
handleSave(item);
}
}}
value={editText}
onChange={(e) => {
setEditText(e.target.value);
}}
onDoubleClick={() => {
dispatch(isEditable({ id: item.id, editable: false }));
}}
variant={"outlined"}
placeholder={item.completed ? <s>{item.title}</s> : item.title}
style={{
flex: 1,
alignSelf: "center"
}}
/>
) : (
<Typography
variant="subtitle2"
style={{
flex: 1,
alignSelf: "center",
fontSize: 16,
paddingLeft: 12
}}
onDoubleClick={() => {
if (editEnabledIdRef.current) {
dispatch(
isEditable({
id: editEnabledIdRef.current,
editable: false
})
);
}
dispatch(isEditable({ id: item.id, editable: true }));
editEnabledIdRef.current = item.id;
setEditText(item.title);
}}
>
{item.completed ? <s>{item.title}</s> : item.title}
</Typography>
)}
{item.editable ? (
<Button
variant="outlined"
size="small"
onClick={() => {
handleSave(item);
}}
>
Save
</Button>
) : (
<Button
variant="outlined"
size="small"
onClick={() => {
dispatch(deleteTask({ id: item.id }));
}}
>
Delete
</Button>
)}
</Stack>
);
})}
</>
);
};
export default ListTodos;
Top comments (0)