Single Page Applications (SPAs) interacts with the backend layer through REST APIs for the general Create-Read-Update-Delete (CRUD) operations. The traditional model involves using the native fetch or libraries such as axios for providing the interface to the REST APIs for the front-end React code.

Enterprise applications built with this architecture is often limited to client side state that represents the data from the backend layer. Let’s set our use case here with the below example of a simple CRUD page with list of employees a panel to add a new user and a panel to update and delete the user.

Visualizes the entire example flow of getting employees, adding, updating and deleting an employee
export function Employees() {
    const [employees, setEmployees] = useState([]);
    const [selectedEmployee, setSelectedEmployee] = useState(null);

    useEffect(() => {
        setEmployees(fetchEmployees);
    }, []);

    return (
        <div>
            <List employees={employees} onSelect={setSelectedEmployee} />
            <AddPanel
                onAdd={(addedEmployee) => {
                    setEmployees({ ...employees, addedEmployee })
                }}
            />
            <UpdatePanel
                employee={selectedEmployee}
                onUpdate={(updatedEmployee) => {
                    setEmployees(employees.map(
                        employee => employee.id === updatedEmployee.id
                            ? updatedEmployee
                            : employee
                    ))
                }}
                onDelete={(id) => {
                    setEmployees(employees.filter(employee => employee.id !== id))
                }}
            />
        </div>
    )
}

The code block above represents the screen visualised in the image before and the updates from the backend is maintained in the local state or in a centralised state management system like Redux or Recoil. We have not shown the code blocks that is responsible for the REST APIs as it’s generally a boiler plate.

React-Query fetches, caches and updates data in our React applications without touching any “global state” – an excerpt from their website here. It doesn’t however replaces a global state management system for huge set of client side states, it instead acts as a state management and cache for the backend data. How React-Query works is not explained here and is better left to their documentation to gather a grasp on. The custom hook using React-Query is given below,

import { useMutation, useQuery, useQueryClient } from "react-query";
import axios from "axios";
import _ from "lodash";

export function useEmployees() {
    const queryClient = useQueryClient();

    const employees = useQuery("employees", async () => {
        const { data } = await axios.get("/api/Employee");
        return data;
    });

    const add = useMutation(
        async (employee) => {
            const { data } = await axios.post("/api/Employee", employee);
            return data;
        },
        {
            onSuccess: (addedEmployee) => {
                queryClient.setQueryData("employees", (currentEmployees) => [
                    ...currentEmployees,
                    addedEmployee,
                ]);
            },
        }
    );

    const update = useMutation(
        async (employee) => {
            const { data } = await axios.put("/api/Employee", employee);
            return data;
        },
        {
            onSuccess: (updatedEmployee) => {
                queryClient.setQueryData("employees", (currentEmployees) =>
                    currentEmployees.map(
                        (employee) => (employee.id === updatedEmployee.id
                            ? updatedEmployee
                            : employee)
                    )
                );
            },
        }
    );

    const remove = useMutation(
        async (id) => {
            const { data } = await axios.delete(`/api/Employee/${id}`);
            return data;
        },
        {
            onSuccess: (id) => {
                queryClient.setQueryData("employees", (currentEmployees) =>
                    currentEmployees.filter((employee) => employee.id !== id)
                );
            },
        }
    );

    return {
        employees,
        add,
        update,
        remove,
    };
}

The custom hook returns an object with a single data representing the list of employees and mutations for adding, updating and deleting an employee. A cached query data is represented by a single key or an array of keys. The cached data can be updated when there is a successful mutation as represented above with the onSuccess callback function that updates the data cached with the “employees” key. Explicitly setting the query data triggers an update to the states for all the components subscribed to the “employees” query data.

The updated React components looks like this with the logics of handling the state changes when there is a server mutation is handled by the custom hook separating the logic and UI into separate units.

export function Employees() {
    const { employees, add, update, remove } = useEmployees();
    const [selectedEmployee, setSelectedEmployee] = useState(null);

    return (
        <div>
            <List employees={employees} onSelect={setSelectedEmployee} />
            <AddPanel onAdd={add} />
            <UpdatePanel employee={selectedEmployee} onUpdate={update} onDelete={remove} />
        </div>
    )
}

The update code for the Employees looks much more compact with the logic moved to a testable JavaScript file away from the User Interface.

Feel free to ping me issues with the code and better improvements on how to handle the CRUD REST APIs in a React Environment

0 Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like