Search code examples
reactjsreduxredux-toolkit

How to implement redux toolkit to register a user?


Hi i am doing an app and i have a sign up page with a form where i register a user and send the data through my post route in my API with nodejs.

I was able to do this without inconvinients but i know that as my app will grow and have several functionalities it will be better if i use redux toolkit.

This is how i am doing it.

import React, { useState } from 'react'
import axios from 'axios'

//Material UI
import {Grid, Container, Box, Button, FormControl, InputLabel, Input, FormHelperText } from '@mui/material'

function Register() {
    
    const [ formData, setFormData ] = useState({
        name: '',
        email: '',
        password: '',
        password2: ''
    })

    const { name, email, password, password2 } = formData; 

    const onChange = e => setFormData({...formData, [e.target.name]: e.target.value})

    const onSubmit = async (e) => {
        e.preventDefault();
        if(password !== password2){
            console.log("Passwords do not match");
        } else {
            const newUser = {
                name: formData.name,
                email: formData.email,
                password: formData.password
            }

            try {
                const config = {
                    headers: {
                        'Content-Type': 'application/json'
                    }
                }

                const body = JSON.stringify(newUser);
                const res = await axios.post('http://localhost:3000/api/users', body, config)
                console.log(res.data)

            } catch (error) {
                console.error(error.response.data)
            }
        
        }
    }

    return (
    <Container>
        <Box p={4}>
            <Grid container  >
                <form onSubmit={e => onSubmit(e)}>
                    <Grid item md={12}>
                        <FormControl>
                            <InputLabel htmlFor="name">Name</InputLabel>
                            <Input 
                            type='text' 
                            id='name' 
                            name='name'
                            value={name}
                            onChange={e => onChange(e)}
                            />
                            <FormHelperText>Insert your complete name</FormHelperText>
                        </FormControl>
                    </Grid> 
                    <Grid item md={12}>
                        <FormControl>
                            <InputLabel htmlFor="email">Email</InputLabel>
                            <Input 
                            id='email' 
                            type='email' 
                            name='email'
                            value={email}
                            onChange={e => onChange(e)}
                            />
                            <FormHelperText>Insert your email</FormHelperText>
                        </FormControl>
                    </Grid>
                    <Grid item md={12}>    
                        <FormControl>
                            <InputLabel htmlFor="password">Password</InputLabel>
                            <Input 
                            id="password" 
                            type='password' 
                            name='password'
                            value={password}
                            onChange={e => onChange(e)}
                            />
                            <FormHelperText>Insert your password (must have at least 6 characters)</FormHelperText>
                        </FormControl>
                    </Grid>
                    <Grid item md={12}>    
                        <FormControl>
                            <InputLabel htmlFor="password2">Confirm Password</InputLabel>
                            <Input 
                            id="password2" 
                            type='password'
                            name='password2' 
                            value={password2}
                            onChange={e => onChange(e)}
                            />
                            <FormHelperText>Insert your password (must have at least 6 characters)</FormHelperText>
                        </FormControl>
                    </Grid>
                    <Grid item md={12}>    
                        <Button type='submit' variant="contained" color="primary">
                            Register
                        </Button>
                    </Grid>
                </form>
            </Grid>
        </Box>
    </Container>
  )
}

export default Register

I now want to try and start to clean up my code and manage state by using redux but i am still very confused about this library and i dont know how to proceed.

I have already created a store and passed it to the provider but i am having trouble to understand slices.

This is what i have up to now:

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
    token: localStorage.getItem('token'),
    isAuthenticated: null,
    loading: true,
    user: null
}

export const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    
  },
})

// Action creators are generated for each case reducer function
export const {  } = counterSlice.actions

export default counterSlice.reducer

So if the data i send through the form comes without errors i get a token. I want to make two reducers functions one to register my user instead of having all that block of code in my component and another to authenticate the user with the token i previously received.


Solution

  • Reducers are the functions that receive the current state and an action object, and compute and return the next state value. Reducers aren't the piece of code that do side-effects like making API calls, this is what actions are for. Actions are dispatched to the store and effect a change to the state. Asynchronous actions are dispatched to the store and handled by some middleware the doesn't immediately hand the action over to the reducer tree. This the part you want to move the user registration logic into.

    See createAsyncThunk

    import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
    import axios from 'axios';
    
    export const registerUser = createAsyncThunk(
      "user/register",
      async (newUser, thunkAPI) => {
        try {
          const config = {
            headers: {
              'Content-Type': 'application/json'
            }
          }
    
          const body = JSON.stringify(newUser);
          const { data } = await axios.post('http://localhost:3000/api/users', body, config);
          console.log(data);
          localStorage.setItem("token", JSON.stringify(data));
          return data;
        } catch (error) {
          console.error(error.response.data);
          return thunkAPI.rejectWithValue(error.response.data);
        }
      },
    );
    
    export const verifyToken = createAsyncThunk(
      "user/verifyToken",
      async (token, thunkAPI) => {
        try {
          const { data } = await axios.get(.....);
          return data;
        } catch(error) {
          return thunkAPI.rejectWithValue(/* error */);
        }
      }
    );
    
    const initialState = {
      token: JSON.parse(localStorage.getItem('token') ?? ""),
      isAuthenticated: null,
      loading: false,
      user: null,
      error: null,
    };
    
    export const userSlice = createSlice({
      name: 'user',
      initialState,
      reducers: {
        ... any regular reducer cases ...
      },
      extraReducers: builder => {
        builder
          .addCase(registerUser.pending, (state) => {
            state.loading = true;
          })
          .addCase(registerUser.fulfilled, (state, action) => {
            state.loading = false;
            state.token = action.payload; // <-- no errors, response is token
          })
          .addCase(registerUser.rejected, (state, action) => {
            state.loading = false;
            state.error = action.payload; // <-- error response
          })
          .addCase(verifyToken.pending, (state) => {
            state.loading = true;
          })
          .addCase(verifyToken.fulfilled, (state, action) => {
            state.loading = false;
            state.user = action.payload;
            state.isAuthenticated = true;
          })
          .addCase(verifyToken.rejected, (state, action) => {
            state.loading = false;
            state.error = action.payload;
            state.isAuthenticated = false;
          });
      },
    });
    
    // Action creators are generated for each case reducer function
    export const {  } = counterSlice.actions;
    
    export default counterSlice.reducer;
    

    I had to sort of hand-wave the token verification/authentication, but this example should be a good starting point.

    The Register component would now use the useDispatch hook an dispatch function to dispatch the registerUser and verifyToken actions to the store, awaiting each one sequentially.

    ...
    import { useDispatch, useSelector } from 'react-redux';
    ...
    import { registerUser, verifyToken } from '../path/to/user.slice';
    
    function Register() {
      const dispatch = useDispatch();
      const isLoading = useSelector(state => state.user.loading);
       
      const [formData, setFormData] = useState({
        name: '',
        email: '',
        password: '',
        password2: ''
      });
    
      ...
    
      const onSubmit = async (e) => {
        e.preventDefault();
    
        if (password !== password2) {
          console.log("Passwords do not match");
        } else {
          const { name, email, password } = formData;
          const newUser = { name, email, password };
    
          try {
            const token = await dispatch(registerUser(newUser)).unwrap();
            await dispatch(verifyToken(token));
            // user successfully registered and token verified/authenticated
            // maybe redirect to home page? 🤷🏻‍♂️ world is your oyster.
          } catch (error) {
            console.error(error);
          }
        }
      };
    
      if (isLoading) {
        return <LoadingSpinner />;
      }
    
      return (
        ...
      );
    }
    
    export default Register;
    

    The pattern of createAsyncThunk and the "pending"/"fulfilled"/"rejected" for API calls is common enough the react-redux team created additional specialized API slices via Redux Toolkit Query (RTKQ). What Redux Toolkit (RTK) did to older React Redux to cut down on boiler plate code, RTKQ does to RTK in terms of asynchronous actions to manage API calls.

    Since you are just starting out in Redux though I'd suggest learning the basics first to get more comfortable, but I highly encourage you to explore RTKQ when you can.