DEV Community

Kshitiz mishra
Kshitiz mishra

Posted on

Todo App with React.js React-Redux and MUI

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.

Todo App Screenshot

Set Up Your React App

npx create-react-app todo-app

Enter fullscreen mode Exit fullscreen mode

Navigate to project directory

cd todo-app
Enter fullscreen mode Exit fullscreen mode

Install react-redux

npx install @redux/toolkit react-redux

Enter fullscreen mode Exit fullscreen mode

Install MUI

npm install @mui/material @emotion/react @emotion/styled
Enter fullscreen mode Exit fullscreen mode

Now you can start the development server by running the following command:

npm start
Enter fullscreen mode Exit fullscreen mode

Project folder structures
App folder structure

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>
);

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

Github code

Top comments (0)