Published: September 10 2021

React + Recoil - Set atom state after async HTTP GET or POST request

Tutorial built with React 17.0.2 and Recoil 0.4.1

This is a quick example of how to set the state of a Recoil atom after sending an async HTTP GET or POST request to an API.

The code snippets in this post are from a React + Recoil auth tutorial I posted recently, to see the code running in a live demo app check out React + Recoil - JWT Authentication Tutorial & Example.

Recoil Overview

Recoil is a new state management library built by the Facebook team that simplifies global state management, it requires much less code than other libraries like Redux, and is easier to learn because it works just like React. The two core concepts of Recoil are atoms and selectors, an atom defines a unit (key) in the global state object of an app, and a selector is function that returns a value that is derived (computed) from atoms and/or other selectors. For more info on Recoil see https://recoiljs.org/docs/introduction/core-concepts.

Actions in the Recoil example app

In the example app, action objects contain methods for performing actions on each content type, for example the user actions object contains methods for login, logout and fetching users. Actions encapsulate all HTTP communication with the API and recoil state update operations, and an action object must be wrapped in a React hook function (e.g. useUserActions()) in order to access Recoil hook functions for retrieving and setting state (e.g. useSetRecoilState(), useRecoilState(), useRecoilValue() etc).

 

User Actions

Path: /src/_actions/user.actions.js

The user actions object returned by the useUserActions() hook function contains methods for login, logout and fetching all users. It handles communication between the React app and the backend api for everything related to users, and handles Recoil state update operations for users and auth atoms. HTTP requests to the API are sent with the fetch wrapper.

A React hook function is required because Recoil hook functions (e.g. useSetRecoilState()) can only be called within React components or hook functions.

import { useSetRecoilState } from 'recoil';

import { history, useFetchWrapper } from '_helpers';
import { authAtom, usersAtom } from '_state';

export { useUserActions };

function useUserActions () {
    const baseUrl = `${process.env.REACT_APP_API_URL}/users`;
    const fetchWrapper = useFetchWrapper();
    const setAuth = useSetRecoilState(authAtom);
    const setUsers = useSetRecoilState(usersAtom);

    return {
        login,
        logout,
        getAll
    }

    function login(username, password) {
        return fetchWrapper.post(`${baseUrl}/authenticate`, { username, password })
            .then(user => {
                // store user details and jwt token in local storage to keep user logged in between page refreshes
                localStorage.setItem('user', JSON.stringify(user));
                setAuth(user);

                // get return url from location state or default to home page
                const { from } = history.location.state || { from: { pathname: '/' } };
                history.push(from);
            });
    }

    function logout() {
        // remove user from local storage, set auth state to null and redirect to login page
        localStorage.removeItem('user');
        setAuth(null);
        history.push('/login');
    }

    function getAll() {
        return fetchWrapper.get(baseUrl).then(setUsers);
    }    
}
 

Fetch Wrapper

Path: /src/_helpers/fetch-wrapper.js

The fetch wrapper is a lightweight wrapper around the native browser fetch() function used to simplify the code for making HTTP requests. It returns an object with methods for get, post, put and delete requests, it automatically handles the parsing of JSON data from responses, and throws an error if the HTTP response is not successful (!response.ok). If the response is 401 Unauthorized or 403 Forbidden the user is automatically logged out of the React + Recoil app.

The authHeader() function is used to automatically add a JWT auth token to the HTTP Authorization header of the request if the user is logged in and the request is to the application API url. The current logged in (auth) state of the user is retrieved from Recoil with a call to useRecoilState(authAtom), the setAuth() function is used in the handleResponse() function to log the user out if required.

With the fetch wrapper a POST request can be made as simply as this: fetchWrapper.post(url, body);. It's used in the example app by user actions.

import { useRecoilState } from 'recoil';

import { history } from '_helpers';
import { authAtom } from '_state';

export { useFetchWrapper };

function useFetchWrapper() {
    const [auth, setAuth] = useRecoilState(authAtom);

    return {
        get: request('GET'),
        post: request('POST'),
        put: request('PUT'),
        delete: request('DELETE')
    };

    function request(method) {
        return (url, body) => {
            const requestOptions = {
                method,
                headers: authHeader(url)
            };
            if (body) {
                requestOptions.headers['Content-Type'] = 'application/json';
                requestOptions.body = JSON.stringify(body);
            }
            return fetch(url, requestOptions).then(handleResponse);
        }
    }
    
    // helper functions
    
    function authHeader(url) {
        // return auth header with jwt if user is logged in and request is to the api url
        const token = auth?.token;
        const isLoggedIn = !!token;
        const isApiUrl = url.startsWith(process.env.REACT_APP_API_URL);
        if (isLoggedIn && isApiUrl) {
            return { Authorization: `Bearer ${token}` };
        } else {
            return {};
        }
    }
    
    function handleResponse(response) {
        return response.text().then(text => {
            const data = text && JSON.parse(text);
            
            if (!response.ok) {
                if ([401, 403].includes(response.status) && auth?.token) {
                    // auto logout if 401 Unauthorized or 403 Forbidden response returned from api
                    localStorage.removeItem('user');
                    setAuth(null);
                    history.push('/login');
                }
    
                const error = (data && data.message) || response.statusText;
                return Promise.reject(error);
            }
    
            return data;
        });
    }    
}
 

Login Component

Path: /src/login/Login.jsx

The login page contains a form built with the React Hook Form library that contains username and password fields for logging into the Next.js app.

Form validation rules are defined with the Yup schema validation library and passed with the formOptions to the React Hook Form useForm() function, for more info on Yup see https://github.com/jquense/yup.

The useForm() hook function returns an object with methods for working with a form including registering inputs, handling form submit, accessing form state, displaying errors and more, for a complete list see https://react-hook-form.com/api/useform.

The onSubmit function gets called when the form is submitted and valid, and submits the user credentials to the api by calling userActions.login(). On successful authentication the user auth data is stored in Recoil shared state by the login method in user actions.

The returned JSX template contains the markup for page including the form, input fields and validation messages. The form fields are registered with the React Hook Form by calling the register function with the field name from each input element (e.g. {...register('title')}). For more info on form validation with React Hook Form see React Hook Form 7 - Form Validation Example.

import { useForm } from "react-hook-form";
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

import { useUserActions } from '_actions';

export { Login };

function Login() {
    const userActions = useUserActions();

    // form validation rules 
    const validationSchema = Yup.object().shape({
        username: Yup.string().required('Username is required'),
        password: Yup.string().required('Password is required')
    });
    const formOptions = { resolver: yupResolver(validationSchema) };

    // get functions to build form with useForm() hook
    const { register, handleSubmit, setError, formState } = useForm(formOptions);
    const { errors, isSubmitting } = formState;

    function onSubmit({ username, password }) {
        return userActions.login(username, password)
            .catch(error => {
                setError('apiError', { message: error });
            });
    }

    return (
        <div className="col-md-6 offset-md-3 mt-5">
            <div className="alert alert-info">
                Username: test<br />
                Password: test
            </div>
            <div className="card">
                <h4 className="card-header">Login</h4>
                <div className="card-body">
                    <form onSubmit={handleSubmit(onSubmit)}>
                        <div className="form-group">
                            <label>Username</label>
                            <input name="username" type="text" {...register('username')} className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.username?.message}</div>
                        </div>
                        <div className="form-group">
                            <label>Password</label>
                            <input name="password" type="password" {...register('password')} className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.password?.message}</div>
                        </div>
                        <button disabled={isSubmitting} className="btn btn-primary">
                            {isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
                            Login
                        </button>
                        {errors.apiError &&
                            <div className="alert alert-danger mt-3 mb-0">{errors.apiError?.message}</div>
                        }
                    </form>
                </div>
            </div>
        </div>
    )
}
 

Home Component

Path: /src/home/Home.jsx

The home page is displayed after signing in to the application, it shows the signed in user's name plus a list of all users in the tutorial application. The users are loaded into Recoil state by calling userActions.getAll() from the useEffect() hook function, see how the users are fetched and the Recoil state is updated in user actions.

Recoil state values are retrieved for auth and users data with the help of the useRecoilValue() hook function.

import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';

import { authAtom, usersAtom } from '_state';
import { useUserActions } from '_actions';

export { Home };

function Home() {
    const auth = useRecoilValue(authAtom);
    const users = useRecoilValue(usersAtom);
    const userActions = useUserActions();

    useEffect(() => {
        userActions.getAll();
        
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    return (
        <div>
            <h1>Hi {auth?.firstName}!</h1>
            <p>You're logged in with React + Recoil & JWT!!</p>
            <h3>Users from secure api end point:</h3>
            {users &&
                <ul>
                    {users.map(user =>
                        <li key={user.id}>{user.firstName} {user.lastName}</li>
                    )}
                </ul>
            }
            {!users && <div className="spinner-border spinner-border-sm"></div>}
        </div>
    );
}

 


Need Some React Help?

Search fiverr for freelance React developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by