DEV Community

Piotr Glejzer
Piotr Glejzer

Posted on

Todo list with React, Redux, TypeScript and drag and drop

In this article I will present how to make simple ToDo application with React, Redux, TypeScript and drag and drop library.

Basicily, Redux is overkill to that simple application as ToDo application but a lot of technical tasks before "job Interview" or even sometimes during live coding require that approach to check your knowledge about Redux + React.

I will use React + TypeScript, I prefer that style of coding on the frontend but if you want you can use JavaScript without TS :)

To styling components I will use MUI with version 5.4.0 and to the drag and drop events I will use react-beautiful-dnd and redux-toolkit for redux.

 

Requirements

  • basic knowledge about JavaScript/TypeScript
  • basic knowledge React
  • basic knowledge Redux
  • basic knowledge about concept css-in-js

These points are not required but if you know how these things works it will be better for your because I will not explain how React / Typescript / MUI works.

 

So, the first step is creating simple Create-React-App with TS template and redux.

npx create-react-app react-todo-list-redux --template redux-typescript
Enter fullscreen mode Exit fullscreen mode

After installation install another required dependencies.

npm i @emotion/react @emotion/styled @mui/icons-material @mui/material react-beautiful-dnd uuid
Enter fullscreen mode Exit fullscreen mode

and to devDependencies

npm i @types/react-beautiful-dnd @types/uuid -D
Enter fullscreen mode Exit fullscreen mode

After installation all required dependencies your application folder should look similar to this:

started-package

and you can delete not nessesary files/folders for this project as

  • App.css
  • App.test.tsx
  • index.css
  • logo.svg
  • serviceWorker.ts
  • setupTests.ts
  • features folder
  • app folder

We will not use them :)

After that create folder redux and inside this folder create another folder called store and inside store folder create file index.ts.

cd src && mkdir redux && cd redux && mkdir store && cd store && touch index.ts
Enter fullscreen mode Exit fullscreen mode

and copy this code below to store/index.ts

import { configureStore, combineReducers } from '@reduxjs/toolkit';

export const store = configureStore({
  reducer: combineReducers({}),
});

export type StoreDispatch = typeof store.dispatch;
export type StoreState = ReturnType<typeof store.getState>;
Enter fullscreen mode Exit fullscreen mode

The code above is storing your reducers and combine them to use in our application but for now reducers are empty.

The next step is start project

If you deleted files you probably will get some errors about deleted files. Just remove every not necessary imports in the App.tsx and in the index.tx files.

To index.tsx import CssBaseLine component from @mui/material/CssBaseline and add as a child to the <Provider />. This component provides normalized styling to the components.

import CssBaseline from '@mui/material/CssBaseline';
Enter fullscreen mode Exit fullscreen mode

app-and-index-photo

After that type in your console:

npm start
Enter fullscreen mode Exit fullscreen mode

If everything is going well, you will see blank page and after that open your console and you probably see error called Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers. - this error is complety fine at the moment because we don't have any reducers yet!

Create new folders

We are gonna create new folders called components, types and slice folder in the src directory. Inside types folder create index.ts file.

In the root directory type:

cd src && mkdir components && mkdir types && cd types && touch index.ts && cd ../redux && mkdir slice
Enter fullscreen mode Exit fullscreen mode

Basic Styling in App.tsx

Open file App.tsx and add this code bellow to your file. This is a basic styling for with container and grid.

// src/App.tsx
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid';

function App() {
  return (
    <Container>
      <Typography textAlign='center' variant='h3' mt={3} mb={5}>
        This is a ToDo APP with Redux
      </Typography>
      <Grid container spacing={3} justifyContent='center'>
        <Grid item md={4}>
          ...
        </Grid>
        <Grid item md={4}>
          ...
        </Grid>
        <Grid item md={4}>
          ...
        </Grid>
      </Grid>
    </Container>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Create new files in components folder

Go to the components folder and insiade create new folder called columns and inside this folder create 3 files called: ToDo.tsx, InProgress.tsx and Done.tsx.

After after that go to folder components and create file called ColumnLayout.tsx and copy code below inside.

// src/components/ColumnLayout.tsx
import React, { useState } from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Box from '@mui/material/Box';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Checkbox from '@mui/material/Checkbox';
import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete';
import Alert from '@mui/material/Alert';
import Collapse from '@mui/material/Collapse';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import { useDispatch } from 'react-redux';
import { StoreDispatch } from '../redux/store';
import { IColumnLayoutProps } from '../types';

const ColumnLayout: React.FC<IColumnLayoutProps> = ({
  labelText,
  addHandler,
  removeHandler,
  completedHandler,
  selectorState,
  droppableId,
}) => {
  const [isError, setIsError] = useState({
    isShow: false,
    text: '',
  });
  const [textDescription, setTextDescription] = useState('');
  const dispatch = useDispatch<StoreDispatch>();

  const handleOnChange = ({
    target: { value },
  }: React.ChangeEvent<HTMLInputElement>) => {
    setTextDescription(value);
    setIsError({
      isShow: value.length > 200,
      text:
        value.length > 200
          ? 'The input value cannot be more than 200 characters'
          : '',
    });
  };

  const handleOnBlur = () => {
    setIsError({ ...isError, isShow: false });
  };

  const handleOnClick = () => {
    if (!isError) {
      dispatch(addHandler(textDescription));
      setTextDescription('');
    }
  };

  const handleInputKeyDown = ({
    target,
    key,
  }: React.KeyboardEvent<HTMLInputElement>) => {
    if (key === 'Enter') {
      if (
        (target as HTMLInputElement).value.length > 0 &&
        (target as HTMLInputElement).value.length <= 200
      ) {
        handleOnClick();
      } else {
        setIsError({
          isShow: true,
          text: 'The input value cannot be empty',
        });
      }
    }
  };

  return (
    <Box borderRadius={1} width='100%' sx={{ boxShadow: 2, p: 3 }}>
      <TextField
        fullWidth
        label={labelText}
        onChange={handleOnChange}
        onBlur={handleOnBlur}
        onKeyDown={handleInputKeyDown}
        value={textDescription}
        variant='outlined'
        size='small'
      />

      <Collapse in={isError.isShow}>
        <Alert severity='error' sx={{ my: 1 }}>
          {isError.text}
        </Alert>
      </Collapse>

      <Box width='100%' display='flex' justifyContent='center'>
        <Button
          size='medium'
          sx={{ my: 1, maxWidth: 200 }}
          variant='outlined'
          color='primary'
          fullWidth
          onClick={handleOnClick}
          disabled={
            textDescription.length === 0 || textDescription.length > 200
          }
        >
          Add Item
        </Button>
      </Box>

      <List sx={{ minHeight: '300px' }}>
        {selectorState.map(
          ({ id, text, isFinished, createdAt, updatedAt }, index: number) => {
            return (
              <ListItem
                sx={{
                  position: 'relative',
                  border: '1px solid #989898',
                  bgcolor: '#fff',
                  my: 1,
                  borderRadius: '3px',
                  '& .MuiTypography-root': {
                    display: 'flex',
                    alignItems: 'center',
                  },
                }}
              >
                <ListItemText
                  sx={{
                    textDecoration: isFinished ? 'line-through' : 'none',
                    wordBreak: 'break-word',
                  }}
                >
                  <IconButton sx={{ p: 1, mr: 1 }}>
                    <ArrowDownwardIcon />
                  </IconButton>

                  <Box
                    component='span'
                    width='100%'
                    position='absolute'
                    top='0'
                    fontSize='.7rem'
                  >
                    {updatedAt ? 'Updated' : 'Created'} at:{' '}
                    {updatedAt || createdAt}
                  </Box>

                  <Box component='span' width='100%'>
                    {text}
                  </Box>

                  <Box display='flex' component='span'>
                    <IconButton onClick={() => dispatch(removeHandler(id))}>
                      <DeleteIcon />
                    </IconButton>
                    <Checkbox
                      edge='end'
                      value={isFinished}
                      checked={isFinished}
                      inputProps={{ 'aria-label': 'controlled' }}
                      onChange={() =>
                        dispatch(
                          completedHandler({
                            isFinished: !isFinished,
                            id,
                            updatedAt: new Date().toLocaleString(),
                          })
                        )
                      }
                    />
                  </Box>
                </ListItemText>
              </ListItem>
            );
          }
        )}
      </List>
    </Box>
  );
};

export default ColumnLayout;
Enter fullscreen mode Exit fullscreen mode
As an explanation

Code above contains styling, events for each column ToDo, In Progress and Done. It's also contains props which will be used in the redux reducers to store states and update everything dynamically.

After that go to the types folder and open index.ts and copy this code below which contains model for state reducers, types for ColumnLayout component and types ActionSlice/TUpdateTextShowed for PayloadAction from @reduxjs/toolkit:

// src/types/index.ts
import { AnyAction } from '@reduxjs/toolkit';

export interface IModel {
  id: string;
  text: string;
  isFinished: boolean;
  createdAt?: string;
  updatedAt?: string;
  isTextShowed?: boolean;
}

export type TActionSlice = Omit<IModel, 'text'>;
export type TUpdateTextShowed = Omit<TActionSlice, 'isFinished'>;

export interface IColumnLayoutProps {
  labelText?: string;
  addHandler: (v: string) => AnyAction;
  removeHandler: (v: string) => AnyAction;
  completedHandler: (v: TActionSlice) => AnyAction;
  selectorState: IModel[];
  droppableId: string;
  updateTextShowed: (v: TUpdateTextShowed) => AnyAction;
}
Enter fullscreen mode Exit fullscreen mode

Update columns files

Go to files Done.tsx, ToDo.tsx and InProgress.tsx in columns folder and copy code below and paste into them:

// src/components/columns/InProgress.tsx
import Typography from '@mui/material/Typography';
import { useSelector } from 'react-redux';
import { StoreState } from '../../redux/store';
import { inProgressSlice } from '../../redux/slice/inProgress';
import ColumnLayout from '../ColumnLayout';

export function InProgressColumn() {
  const { inProgress } = useSelector((state: StoreState) => state);

  const {
    actions: { completeStatus, remove, add, updateTextShowed },
  } = inProgressSlice;

  return (
    <>
      <Typography mb={3}>All inProgress tasks: {inProgress.length}</Typography>
      <ColumnLayout
        droppableId='inProgress'
        labelText="Type 'in progress' item"
        completedHandler={completeStatus}
        removeHandler={remove}
        addHandler={add}
        selectorState={inProgress}
        updateTextShowed={updateTextShowed}
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
// src/components/columns/Done.tsx
import Typography from '@mui/material/Typography';
import { useSelector } from 'react-redux';
import { StoreState } from '../../redux/store';
import { doneSlice } from '../../redux/slice/done';
import ColumnLayout from '../ColumnLayout';

export function DoneColumn() {
  const { done } = useSelector((state: StoreState) => state);
  const {
    actions: { completeStatus, remove, add, updateTextShowed },
  } = doneSlice;

  return (
    <>
      <Typography mb={3}>All done tasks: {done.length}</Typography>
      <ColumnLayout
        droppableId='done'
        labelText="Type 'done' item"
        completedHandler={completeStatus}
        removeHandler={remove}
        addHandler={add}
        selectorState={done}
        updateTextShowed={updateTextShowed}
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
// src/components/columns/ToDo.tsx
import Typography from '@mui/material/Typography';
import { useSelector } from 'react-redux';
import { StoreState } from '../../redux/store';
import { todoSlice } from '../../redux/slice/todo';
import ColumnLayout from '../ColumnLayout';

export function ToDoColumn() {
  const { todo } = useSelector((state: StoreState) => state);
  const {
    actions: { completeStatus, remove, add, updateTextShowed },
  } = todoSlice;

  return (
    <>
      <Typography mb={3}>All todo tasks: {todo.length}</Typography>
      <ColumnLayout
        droppableId='todo'
        labelText="Type 'to do' item"
        completedHandler={completeStatus}
        removeHandler={remove}
        addHandler={add}
        selectorState={todo}
        updateTextShowed={updateTextShowed}
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
As an explanation

Code above contains resuable ColumnLayout component which have props for update redux state.

After that go to App.tsx file and change every ... to the column components.

// src/App.tsx
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid';
import { ToDoColumn } from './components/columns/ToDo';
import { InProgressColumn } from './components/columns/InProgress';
import { DoneColumn } from './components/columns/Done';

function App() {
  return (
    <Container>
      <Typography textAlign='center' variant='h3' mt={3} mb={5}>
        This is a ToDo APP with Redux
      </Typography>
      <Grid container spacing={3} justifyContent='center'>
        <Grid item md={4}>
          <ToDoColumn />
        </Grid>
        <Grid item md={4}>
          <InProgressColumn />
        </Grid>
        <Grid item md={4}>
          <DoneColumn />
        </Grid>
      </Grid>
    </Container>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Create slices in the redux folder

If you run your application you will see a few errors about not exists code. We will gonna fix this now.

Go to the redux and slice folder and create 3 files called: done.ts, inProgress.ts and todo.ts.

Each files contains own state from createSlice() method and actions with reducers. So we need to create methods that allows to add/update/remove state in the columns.

// src/redux/slice/done.ts
import { createCustomSlice } from './customSlice';

export const doneSlice = createCustomSlice('done');
Enter fullscreen mode Exit fullscreen mode
// src/redux/slice/inProgress.ts
import { createCustomSlice } from './customSlice';

export const inProgressSlice = createCustomSlice('progress');
Enter fullscreen mode Exit fullscreen mode
// src/redux/slice/todo.ts
import { createCustomSlice } from './customSlice';

export const todoSlice = createCustomSlice('todo');
Enter fullscreen mode Exit fullscreen mode

and create file customSlice.ts in the slice folder

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { v4 as uuidv4 } from 'uuid';
import { TActionSlice, TUpdateTextShowed, IModel } from '../../types';

const initialState: IModel[] = [];

export const createCustomSlice = (name: string) => {
  const {
    actions: { add, remove, completeStatus, reorder, update, updateTextShowed },
    reducer,
  } = createSlice({
    name,
    initialState,
    reducers: {
      add: {
        reducer: (state, action: PayloadAction<IModel>) => {
          state.push(action.payload);
        },
        prepare: (text: string) => ({
          payload: {
            id: uuidv4(),
            text,
            isFinished: false,
            createdAt: new Date().toLocaleString(),
            isTextShowed: false,
          } as IModel,
        }),
      },
      update(state, action) {
        state.splice(
          action.payload.destination.index,
          0,
          action.payload.filterState
        );
      },
      remove(state, action: PayloadAction<string>) {
        const index = state.findIndex(({ id }) => id === action.payload);
        state.splice(index, 1);
      },
      completeStatus(state, action: PayloadAction<TActionSlice>) {
        const index = state.findIndex(({ id }) => id === action.payload.id);
        state[index].isFinished = action.payload.isFinished;
        state[index].updatedAt = action.payload.updatedAt;
      },
      updateTextShowed(state, action: PayloadAction<TUpdateTextShowed>) {
        const index = state.findIndex(({ id }) => id === action.payload.id);
        state[index].isTextShowed = action.payload.isTextShowed;
      },
      reorder(state, action) {
        const [removed] = state.splice(action.payload.source.index, 1);
        state.splice(action.payload.destination.index, 0, removed);
      },
    },
  });

  return {
    actions: { add, remove, completeStatus, reorder, update, updateTextShowed },
    reducer,
  };
};
Enter fullscreen mode Exit fullscreen mode

This file contains all nessesary reducers, actions and initial state required to propery works redux state mangement to update store. Each file corresponds to own state.

After update files in the slice folder, you need to go to to the store folder and in the index.ts file import reducers from slice folder and combine them together in the store.

// src/redix/store/index.ts
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import { doneSlice } from "../slice/done";
import { inProgressSlice } from "../slice/inProgress";
import { todoSlice } from "../slice/todo";

export const store = configureStore({
  reducer: combineReducers({
    done: doneSlice.reducer,
    inProgress: inProgressSlice.reducer,
    todo: todoSlice.reducer,
  }),
});

export type StoreDispatch = typeof store.dispatch;
export type StoreState = ReturnType<typeof store.getState>;
Enter fullscreen mode Exit fullscreen mode

If everything went well you should have a working ToDo app but still without drag and drop!

basic-app

Update files about drag and drop!

We need to modify a few files to get working drag and drop in our simple application. We are using react-beautiful-dnd which required a DragDropContext as parent component to each Draggable component.

So first, you need to go to App.tsx and replace existing code with this:

// src/App.tsx
import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { DragDropContext, DropResult } from 'react-beautiful-dnd';
import { useDispatch, useSelector } from 'react-redux';
import { ToDoColumn } from './components/columns/ToDo';
import { DoneColumn } from './components/columns/Done';
import { InProgressColumn } from './components/columns/InProgress';
import { todoSlice as todo } from './redux/slice/todo';
import { inProgressSlice as inProgress } from './redux/slice/inProgress';
import { doneSlice as done } from './redux/slice/done';
import { StoreState } from './redux/store';
import { IModel } from './types';

type TAllSilces = 'todo' | 'inProgress' | 'done';

function App() {
  const dispatch = useDispatch();
  const appState = useSelector((state: StoreState) => state);

  const onDragEnd = (result: DropResult) => {
    if (!result.destination) {
      return;
    }

    const { destination, source, draggableId } = result;
    const allSlices = { todo, inProgress, done };

    if (destination.droppableId === source.droppableId) {
      dispatch(
        allSlices[destination.droppableId as TAllSilces].actions.reorder(result)
      );
    } else {
      const [filterState] = (
        (appState as any)[source.droppableId] as IModel[]
      ).filter(({ id }) => id === draggableId);

      dispatch(
        allSlices[source.droppableId as TAllSilces].actions.remove(draggableId)
      );
      dispatch(
        allSlices[destination.droppableId as TAllSilces].actions.update({
          ...result,
          filterState,
        })
      );
    }
  };

  return (
    <Container>
      <Typography textAlign='center' variant='h3' mt={3} mb={5}>
        This is a ToDo APP with Redux
      </Typography>{' '}
      <Grid container spacing={3} justifyContent='center'>
        <DragDropContext onDragEnd={(res) => onDragEnd(res)}>
          <Grid item md={4}>
            <ToDoColumn />
          </Grid>
          <Grid item md={4}>
            <InProgressColumn />
          </Grid>
          <Grid item md={4}>
            <DoneColumn />
          </Grid>
        </DragDropContext>
      </Grid>
    </Container>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The above file was updated about DragDropContext as a mentioned eariler and also with onDragEnd function which is a function when dragging and dropping are over.

This function also check current id's of droppable with destination and source. If destination is equal to source and indexes are different so it gona change position of dragging element in the list. If destinatnion and source is not equal so there are dynamic funtions where depends where droppableId is equal that will fire update and remove reducers to update store.

Next you have to go to the ColumnLayout.tsx file and replace existing code with this:

// src/components/ColumnLayout.tsx
import React, { useState } from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Box from '@mui/material/Box';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Checkbox from '@mui/material/Checkbox';
import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete';
import Alert from '@mui/material/Alert';
import Collapse from '@mui/material/Collapse';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import { useDispatch } from 'react-redux';
import { Droppable, Draggable } from 'react-beautiful-dnd';
import { StoreDispatch } from '../redux/store';
import { IColumnLayoutProps } from '../types';

const ColumnLayout: React.FC<IColumnLayoutProps> = ({
  labelText,
  addHandler,
  removeHandler,
  completedHandler,
  selectorState,
  droppableId,
  updateTextShowed,
}) => {
  const [isError, setIsError] = useState({
    isShow: false,
    text: '',
  });

  const [textDescription, setTextDescription] = useState('');
  const dispatch = useDispatch<StoreDispatch>();

  const handleOnChange = ({
    target: { value },
  }: React.ChangeEvent<HTMLInputElement>) => {
    setTextDescription(value);

    setIsError({
      isShow: value.length > 200,
      text:
        value.length > 200
          ? 'The input value cannot be more than 200 characters'
          : '',
    });
  };

  const handleOnBlur = () => {
    setIsError({ ...isError, isShow: false });
  };

  const handleOnClick = () => {
    if (!isError.isShow) {
      dispatch(addHandler(textDescription));
      setTextDescription('');
    }
  };

  const handleInputKeyDown = ({
    target,
    key,
  }: React.KeyboardEvent<HTMLInputElement>) => {
    if (key === 'Enter') {
      if (
        (target as HTMLInputElement).value.length > 0 &&
        (target as HTMLInputElement).value.length <= 200
      ) {
        handleOnClick();
      } else {
        setIsError({
          isShow: true,
          text: 'The input value cannot be empty',
        });
      }
    }
  };

  return (
    <Box borderRadius={1} width='100%' sx={{ boxShadow: 2, p: 3 }}>
      <TextField
        fullWidth
        label={labelText}
        onChange={handleOnChange}
        onBlur={handleOnBlur}
        onKeyDown={handleInputKeyDown}
        value={textDescription}
        variant='outlined'
        size='small'
      />

      <Collapse in={isError.isShow}>
        <Alert severity='error' sx={{ my: 1 }}>
          {isError.text}
        </Alert>
      </Collapse>

      <Box width='100%' display='flex' justifyContent='center'>
        <Button
          size='medium'
          sx={{ my: 1, maxWidth: 200 }}
          variant='outlined'
          color='primary'
          fullWidth
          onClick={handleOnClick}
          onKeyDown={({ key }) => key === 'Enter' && handleOnClick()}
          disabled={
            textDescription.length === 0 || textDescription.length > 200
          }
        >
          Add Item
        </Button>
      </Box>
      <Droppable droppableId={droppableId}>
        {(provided) => (
          <List
            sx={{
              minHeight: '300px',
              li: {
                flexDirection: 'column',
              },
              '& .MuiListItemText-root': {
                width: '100%',
              },
            }}
            ref={provided.innerRef}
            {...provided.droppableProps}
          >
            {selectorState.map(
              (
                { id, text, isFinished, createdAt, updatedAt, isTextShowed },
                index: number
              ) => (
                <Draggable key={id} draggableId={id} index={index}>
                  {(provided, snapshot) => (
                    <ListItem
                      sx={{
                        transition: '.3s ease background-color',
                        color: snapshot.isDragging ? '#fff' : '#000',
                        bgcolor: snapshot.isDragging ? '#000' : '#fff',
                        position: 'relative',
                        border: '1px solid #989898',
                        my: 1,
                        borderRadius: '3px',
                        '& .MuiTypography-root': {
                          display: 'flex',
                          alignItems: 'center',
                        },
                      }}
                      ref={provided.innerRef}
                      {...provided.draggableProps}
                      {...provided.dragHandleProps}
                    >
                      <ListItemText
                        sx={{
                          textDecoration: isFinished ? 'line-through' : 'none',
                          wordBreak: 'break-word',
                        }}
                      >
                        <IconButton
                          sx={{ p: 1, mr: 1 }}
                          onClick={() =>
                            dispatch(
                              updateTextShowed({
                                id,
                                isTextShowed: !isTextShowed,
                              })
                            )
                          }
                        >
                          <ArrowDownwardIcon
                            sx={{
                              color: snapshot.isDragging ? '#fff' : '#000',
                              transform: !isTextShowed ? 'rotate(180deg)' : '',
                            }}
                          />
                        </IconButton>

                        <Box
                          component='span'
                          width='100%'
                          position='absolute'
                          top='0'
                          fontSize='.7rem'
                        >
                          {updatedAt ? 'Updated' : 'Created'} at:{' '}
                          {updatedAt || createdAt}
                        </Box>

                        <Box component='span' width='100%'>
                          {text}
                        </Box>

                        <Box display='flex' component='span'>
                          <IconButton
                            onClick={() => dispatch(removeHandler(id))}
                          >
                            <DeleteIcon
                              sx={{
                                color: snapshot.isDragging ? '#fff' : '#000',
                              }}
                            />
                          </IconButton>
                          <Checkbox
                            edge='end'
                            value={isFinished}
                            checked={isFinished}
                            inputProps={{ 'aria-label': 'controlled' }}
                            onChange={() =>
                              dispatch(
                                completedHandler({
                                  isFinished: !isFinished,
                                  id,
                                  updatedAt: new Date().toLocaleString(),
                                })
                              )
                            }
                          />
                        </Box>
                      </ListItemText>
                      <Collapse in={isTextShowed}>
                        You can add here some content{' '}
                        <span role='img' aria-label='emoji'>
                          😍
                        </span>
                      </Collapse>
                    </ListItem>
                  )}
                </Draggable>
              )
            )}
            {provided.placeholder}
          </List>
        )}
      </Droppable>
    </Box>
  );
};

export default ColumnLayout;
Enter fullscreen mode Exit fullscreen mode

The file above was updated with Draggable and Droppable components. Draggable component required unique draggableId to recognize which element is current dragging and index to update state when drag will end. Whereas, the Droppable component required droppableId which also is unique and recognize a place when the components are dragged and dropped.

The app structure should looks similar to this in the end:

app-strucutre


Conclusion

This application is very simple and contains basic approach how to use Redux + React + Drag And Drop. You can freely modify this code and if you find some bugs let me know.

Here is a link to the github repository with full code:
https://github.com/pglejzer/react-todo-list-redux
and link to the codesanbox

Top comments (2)

Collapse
 
peymanahmadi profile image
Peyman Ahmadi

Thank you for your amazing work!

Collapse
 
fpaghar profile image
Fatemeh Paghar

The project structure is well-organized, allowing for modularity and scalability. The use of TypeScript enhances code clarity, making it a great choice for developers who prefer strong typing on the front end.

The creation of custom slices for each column (todo, in progress, done) showcases a dynamic and scalable approach. This architecture makes it easy to add new columns or extend functionality in the future.