DEV Community

loading...
Cover image for Up & Running with React, Redux Toolkit, Typescript and React Router

Up & Running with React, Redux Toolkit, Typescript and React Router

ogzhanolguncu profile image Oğuzhan Olguncu Originally published at ogzhanolguncu.com Updated on ・6 min read

In this article, we will learn how to use React, Typescript and Redux Toolkit together. The goal is to build a basic CRUD app called Library App where we store our book's authors and titles, and while doing so, I will demonstrate the ease of using Typescript with other technologies. I won't be diving into details of Redux, but rather show how RTK (Redux Toolkit) simplifies our lives. We will also
use React Router to navigate between pages and Chakra UI to build our basic UI.

I'm hoping by the end of this article you'll find RTK and Typescript less intimidating and have more courage to start your next project with these technologies.

I'm assuming you have basic knowledge of React and React Router.

Let's install all the dependencies we:

yarn add @chakra-ui/icons @chakra-ui/react @emotion/react @emotion/styled @reduxjs/toolkit framer-motion react-redux react-router-dom uuid @types/react-redux @types/react-router-dom @types/uuid
Enter fullscreen mode Exit fullscreen mode

Project structure:

├─ src
│  ├─ App.tsx
│  ├─ components
│  │  ├─ BookInfo.tsx
│  │  └─ Navbar.tsx
│  ├─ hooks
│  │  └─ index.ts
│  ├─ index.tsx
│  ├─ pages
│  │  ├─ AddBook.tsx
│  │  └─ BookList.tsx
│  ├─ react-app-env.d.ts
│  ├─ redux
│  │  ├─ bookSlice.ts
│  │  └─ store.ts
│  └─ types.d.ts
Enter fullscreen mode Exit fullscreen mode

Let's start with index.js first. We will set up our Redux and Chakra UI provider.

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
import { Provider } from 'react-redux';
import { store } from './redux/store';

const theme = extendTheme({
  // Set background to blackish color.
  styles: {
    global: {
      'html, body': {
        backgroundColor: 'rgb(26,32,44)',
      },
    },
  },
});

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <ChakraProvider theme={theme}>
        <App />
      </ChakraProvider>
    </Provider>
  </React.StrictMode>,
  document.getElementById('root'),
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
Enter fullscreen mode Exit fullscreen mode

Let's define our store and slice(reducer).

store.ts

import { configureStore } from '@reduxjs/toolkit';
import { bookSlice } from './bookSlice';

export const store = configureStore({
  reducer: {
    book: bookSlice.reducer,
  },
});

export type RootState = ReturnType<typeof store.getState>; // A global type to access reducers types
export type AppDispatch = typeof store.dispatch; // Type to access dispatch
Enter fullscreen mode Exit fullscreen mode

Now, let's move onto our reducer.

bookSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from './store';
import { v4 as uuidv4 } from 'uuid';
import { BookState } from '../types';

//Defining our initialState's type
type initialStateType = {
  bookList: BookState[];
};

const bookList: BookState[] = [
  {
    id: uuidv4(),
    title: '1984',
    author: 'George Orwell',
  },
  {
    id: uuidv4(),
    title: "Harry Potter and the Philosopher's Stone",
    author: 'J. K. Rowling',
  },
  {
    id: uuidv4(),
    title: 'The Lord of the Rings',
    author: 'J.R.R Tolkien',
  },
];

const initialState: initialStateType = {
  bookList,
};

export const bookSlice = createSlice({
  name: 'book',
  initialState,
  reducers: {
    addNewBook: (state, action: PayloadAction<BookState>) => {
      state.bookList.push(action.payload);
    },
    updateBook: (state, action: PayloadAction<BookState>) => {
      const {
        payload: { title, id, author },
      } = action;

      state.bookList = state.bookList.map((book) =>
        book.id === id ? { ...book, author, title } : book,
      );
    },
    deleteBook: (state, action: PayloadAction<{ id: string }>) => {
      state.bookList = state.bookList.filter((book) => book.id !== action.payload.id);
    },
  },
});

// To able to use reducers we need to export them.
export const { addNewBook, updateBook, deleteBook } = bookSlice.actions;

//Selector to access bookList state.
export const selectBookList = (state: RootState) => state.book.bookList;

export default bookSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Our bookSlice accepts name as a key to distinguish this particular slice, initialState to kick-start the slice and, of course, reducers where we define our
actions. reducer functions, just like regular reducers accepts state and action, but since we are using Typescript we also need to define types for our PayloadAction.
Let's quickly define our types in d.ts file.

types.d.ts

export type BookState = {
  id: string;
  title: string | undefined;
  author: string | undefined;
};
Enter fullscreen mode Exit fullscreen mode

And, of course, create a file for hooks as well.

hooks/index.ts

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { RootState, AppDispatch } from '../redux/store';

//useDispatch hook with types.
export const useAppDispatch = () => useDispatch<AppDispatch>();
//useSelector hook with types
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Enter fullscreen mode Exit fullscreen mode

We've finished the Redux and hooks part. It's time to move on to components. Now, all we have to do is create two component, one for Navbar and one for BookInfo, to display the book's data.

Navbar.tsx

import { Button, Flex, Box, Text } from '@chakra-ui/react';
import { Link } from 'react-router-dom';

const Navbar = () => {
  return (
    <Flex
      flexDirection="row"
      justifyContent="space-between"
      alignItems="center"
      width="100%"
      as="nav"
      p={4}
      mx="auto"
      maxWidth="1150px"
    >
      <Box>
        <Link to="/">
          <Button
            fontWeight={['medium', 'medium', 'medium']}
            fontSize={['xs', 'sm', 'lg', 'xl']}
            variant="ghost"
            _hover={{ bg: 'rgba(0,0,0,.2)' }}
            padding="1"
            color="white"
            letterSpacing="0.65px"
          >
            <Text fontSize={['xl', '2xl', '2xl', '2xl']} mr={2}>
              🦉
            </Text>
            Library App
          </Button>
        </Link>
      </Box>

      <Box>
        <Link to="/">
          <Button
            fontWeight={['medium', 'medium', 'medium']}
            fontSize={['xs', 'sm', 'lg', 'xl']}
            variant="ghost"
            _hover={{ bg: 'rgba(0,0,0,.2)' }}
            p={[1, 4]}
            color="white"
          >
            List Books
          </Button>
        </Link>
        <Link to="/add-new-book">
          <Button
            fontWeight={['medium', 'medium', 'medium']}
            fontSize={['xs', 'sm', 'lg', 'xl']}
            variant="ghost"
            _hover={{ bg: 'rgba(0,0,0,.2)' }}
            p={[1, 4]}
            color="white"
          >
            Add Book
          </Button>
        </Link>
      </Box>
    </Flex>
  );
};

export default Navbar;
Enter fullscreen mode Exit fullscreen mode

A simple navbar component that contains links to navigate between pages.

BookInfo.tsx

import { DeleteIcon, EditIcon } from '@chakra-ui/icons';
import { Box, Heading, IconButton, Text } from '@chakra-ui/react';

import { useAppDispatch } from '../hooks';
import { deleteBook } from '../redux/bookSlice';
import { useHistory } from 'react-router-dom';

const BookInfo = ({
  title,
  author,
  id,
  ...rest
}: {
  title: string | undefined,
  author: string | undefined,
  id: string,
}) => {
  const dispatch = useAppDispatch(); // To able to call reducer, functions we use our hook called useAppDispatch
  const history = useHistory();

  //Redirecting user to /update-book route with id parameter.
  const redirect = (id: string) => {
    history.push(`/update-book/${id}`);
  };

  return (
    <Box p={5} justifyContent="space-between" d="flex" shadow="md" borderWidth="1px" {...rest}>
      <Box d="flex" flexDirection="column">
        <Heading fontSize="xl">{title}</Heading>
        <Text mt={4}>{author}</Text>
      </Box>
      <Box>
        <IconButton
          color="#1a202c"
          aria-label=""
          icon={<DeleteIcon />}
          marginRight="1rem"
          onClick={() => dispatch(deleteBook({ id }))}
        />
        <IconButton
          color="#1a202c"
          aria-label=""
          icon={<EditIcon />}
          onClick={() => redirect(id)}
        />
      </Box>
    </Box>
  );
};

export default BookInfo;
Enter fullscreen mode Exit fullscreen mode

We now need a place to use our components. Therefore, we will create two page component BookList page
to display books in our library and AddBook to add new books and update the old ones.

BookList.tsx

import { Box, Button, Flex, Heading, Stack } from '@chakra-ui/react';

import { Link } from 'react-router-dom';
import { useAppSelector } from '../hooks';
import BookInfo from '../components/BookInfo';

const BookList = () => {
  // If we had any other state like book, we could have select it same way we select book. For example, author would be  useAppSelector((state) => state.author.authorNames)
  const bookList = useAppSelector((state) => state.book.bookList);

  return (
    <Flex height="100vh" justifyContent="center" alignItems="center" flexDirection="column">
      <Box width="50%">
        <Box d="flex" flexDirection="row" justifyContent="space-between" marginBottom="20px">
          <Heading color="white">Book List</Heading>
          <Link to="/add-new-book">
            <Button paddingX="3rem">Add</Button>
          </Link>
        </Box>
        <Box rounded="md" bg="purple.500" color="white" px="15px" py="15px">
          <Stack spacing={8}>
            {bookList.map((book) => (
              <BookInfo key={book.id} title={book.title} author={book.author} id={book.id} />
            ))}
          </Stack>
        </Box>
      </Box>
    </Flex>
  );
};

export default BookList;
Enter fullscreen mode Exit fullscreen mode

We've used BookInfo component that we defined earlier.

AddBook.tsx

import { Box, Button, Flex, FormControl, FormLabel, Heading, Input } from '@chakra-ui/react';

import { useState } from 'react';
import { useAppDispatch, useAppSelector } from '../hooks';
import { addNewBook, updateBook } from '../redux/bookSlice';
import { v4 as uuidv4 } from 'uuid';
import { useParams, useHistory } from 'react-router-dom';

const AddBook = () => {
  const { id } = useParams<{ id: string }>(); //If user comes from /update-book, we will catch id of that book here.
  const history = useHistory();
  const dispatch = useAppDispatch();
  const book = useAppSelector((state) => state.book.bookList.find((book) => book.id === id)); // Selecting particular book's information to prefill inputs for updating.

  const [title, setTitle] = useState<string | undefined>(book?.title || ''); // We are initializing useStates if book variable has title or author.
  const [author, setAuthor] = useState<string | undefined>(book?.author || '');

  const handleOnSubmit = () => {
    if (id) {
      editBook();
      return;
    }
    dispatch(addNewBook({ author, title, id: uuidv4() }));
    clearInputs();
  };

  const editBook = () => {
    dispatch(updateBook({ author, title, id }));
    clearInputs();
    history.push('/');
  };

  const clearInputs = () => {
    setTitle('');
    setAuthor('');
  };

  return (
    <Flex height="100vh" justifyContent="center" alignItems="center" flexDirection="column">
      <Box width="50%">
        <Box d="flex" flexDirection="row" justifyContent="space-between" marginBottom="20px">
          <Heading color="white">Add Book</Heading>
        </Box>
        <FormControl isRequired>
          <FormLabel color="white">Title</FormLabel>
          <Input
            value={title}
            color="white"
            placeholder="The Lord of the Rings"
            onChange={(e) => setTitle(e.currentTarget.value)}
          />
          <FormLabel color="white" marginTop={4}>
            Author
          </FormLabel>
          <Input
            value={author}
            color="white"
            placeholder="J.R.R Tolkien"
            onChange={(e) => setAuthor(e.currentTarget.value)}
          />
        </FormControl>
        <Button marginTop={4} colorScheme="teal" type="submit" onClick={handleOnSubmit}>
          Submit
        </Button>
      </Box>
    </Flex>
  );
};

export default AddBook;
Enter fullscreen mode Exit fullscreen mode

This one is a bit trickier than BookList. Since we do adding and updating operations on the same page it may look complicated and bloated at first, but it's quite simple and elegant. All we do is; if there are any
data such as author, title meaning we are editing the book, and we fill inputs accordingly. If there is no data, we enter a title and author and add them to bookList with dispatch action.

It's time to merge all of them into one.

App.tsx

import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';

import Navbar from './components/Navbar';
import AddBook from './pages/AddBook';
import BookList from './pages/BookList';

function App() {
  return (
    <Router>
      <Navbar />
      <Switch>
        <Route path="/" exact component={BookList} />
        <Route path="/add-new-book" component={AddBook} />
        <Route path="/update-book/:id" component={AddBook} />
      </Switch>
    </Router>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

We now have a working project with React, Typescript and Redux Toolkit. Thanks for bearing with me. I hope I encouraged you to use RTK with Typescript in your next project.

Thanks for reading 🥳🥳🥳.

Discussion (2)

pic
Editor guide
Collapse
markerikson profile image
Mark Erikson

Nice! Note that you can simplify the updateBook case, by directly finding and mutating an existing book object. I'd write it as:

updateBook(state, action: PayloadAction<BookState>) {
  const {title, id, author} = action.payload;
  const book = state.booklist.find(book => book.id === id);
  if (book) {
    book.author = author;
    book.title = title;
    // Alternately: Object.assign(book, {title, author})
  }
}

Enter fullscreen mode Exit fullscreen mode

Note that you could also simplify this further using RTK's createEntityAdapter API:

const booksAdapter = createEntityAdapter<BookState>();

const initialState = booksAdapter.getInitialState();

const bookSlice = createSlice({
  name: 'book',
  initialState,
  reducers: {
    addNewBook: booksAdapter.addOne,
    updateBook(state, action: PayloadAction<BookState>) {
      const {id, title, author} = action.payload;
      booksAdapter.updateOne({id, changes: {title, author});
    },
    deleteBook: booksAdapter.removeOne
  }
})
Enter fullscreen mode Exit fullscreen mode

and the adapter reducer functions will handle the add/update/remove logic for you.


Enter fullscreen mode Exit fullscreen mode
Collapse
ogzhanolguncu profile image
Oğuzhan Olguncu Author

That's a great addition, thank you!