React Recoil selector not triggering its get when the atom state updates to a previous value

3.5k views Asked by At

I've started learning React and Recoil by doing a quick project of my own. A single page website that has buttons where each button, in short, make a REST call and a table is created with the data returned.

When a button is clicked, it updates an atom that stores the URL for the given button. A selector is subscribed to this atom and triggers an async REST request to the API. From there, the data returned is processed into a table.

On the first click of each button, everything works as intended. A call to the specific URL is made and the table generated. Issue being, if a button is clicked a second time, the previously data fetched is displayed. I'm unsure if I'm missing an important step and debugging hasn't really revealed anything to me.

Atoms class:

import {atom} from 'recoil'

export const currentTableState = atom({
    key: 'currentTableState',
    default: 'Employee',
})

export const currentUrlState = atom({
    key: 'currentUrlState',
    default: '/employee/find/all?page=0&size=12'
})

export const currentPageState = atom({
    key: 'currentPageState',
    default: 0
})

export const currentSizeState = atom({
    key: 'currentSizeState',
    default: 12
})

Selectors class:

import {selector} from "recoil";

import {currentUrlState} from "./atoms";


export const tableData = selector({
    key: "currentContactDetails",
    get: async ({get}) => {
        const response = await fetch(String(get(currentUrlState)));
        const json = await response.json();
        return json.content;
    }
});

NavBar class containing the buttons:

import React from "react";
import './topNavBar.css';
import {useSetRecoilState, useRecoilValue} from "recoil";

import {currentPageState, currentSizeState, currentTableState, currentUrlState} from "../recoil/atoms";

const TopNavBar = () => {
    const setCurrentTable = useSetRecoilState(currentTableState);
    const setUrl = useSetRecoilState(currentUrlState);
    const page = useRecoilValue(currentPageState);
    const size = useRecoilValue(currentSizeState);

    function updateState(table) {
        setCurrentTable(table);
        if (table.includes(" ")) {
            let split = table.split(" ");
            table = split[1] + '/' + split[0];
        }
        setUrl(`${table.toLowerCase()}/find/all?page=${page}&size=${size}`);
    }

    return (
        <div className='top-nav'>
            <button onClick={() => updateState("Employee")}>Employee
            </button>
            <button onClick={() => updateState("Material")}>Material
            </button>
            <button onClick={() => updateState("Equipment")}>Equipment
            </button>
            <button onClick={() => updateState("Material Request")}>Material Request
            </button>
            <button onClick={() => updateState("Equipment Request")}>Equipment Request
            </button>
        </div>
    )
}

export default TopNavBar;

Table class which generates the tables:

import React from "react";
import './table.css'
import {useRecoilState, useRecoilValueLoadable} from "recoil";

import {currentTableState} from "../recoil/atoms";
import {tableData} from "../recoil/selectors";


const Table = () => {
    const [currentTable] = useRecoilState(currentTableState);
    const items = useRecoilValueLoadable(tableData);

    const Data = () => {
        switch (items.state) {
            case 'loading':
                return <div>Loading...</div>;
            case 'hasError':
                throw items.contents;
            case 'hasValue':
                return <ConstructTable items={items.contents}/>
            default:
                return <div>Something went terribly wrong.</div>
        }
    }

    const TableHeaders = (headers) => <tr>{headers.headers.map(key => <th key={key.toString()}>{key}</th>)}</tr>;
    const TableRows = (items) =>
        items.items.map(row =>
            <tr className='body' key={row.id}>
                {Object.values(row).map((cell, cellIndex) => <td key={cellIndex}>{cell}</td>)}
            </tr>
        )

    const ConstructTable = (content) => {
        let {items} = content
        if (items.length === 0) return <div>{currentTable} table has no data...</div>
        else return (
            <div className="table">
                <table>
                    <thead>
                    <tr>
                        <th
                            className="table-name"
                            colSpan={Object.keys(items[0]).length}
                        >
                            {currentTable}
                        </th>
                    </tr>
                    <TableHeaders headers={Object.keys(items[0])}/>
                    </thead>
                    <tbody>
                    <TableRows items={items}/>
                    </tbody>
                </table>
            </div>
        );
    }
    return <Data/>
}

export default Table;

UPDATE: I got around the issue by forgoing the use of the selectors and mixing hooks and atoms to get a similar functionality, like so:

    const url = useRecoilValue(currentUrlState);
    const table = useRecoilValue(currentTableState);
    const [data, setData] = useRecoilState(currentTableData);
    const [requestState, setRequestState] = useState('loading');

    useEffect(() => {
        console.debug('use effect triggered')
        setRequestState('loading');
        fetch(String(url))
            .then(res => res.json())
            .then(
                (result) => {
                    setData(result.content);
                    setRequestState('loaded');
                },
                (error) => setRequestState(error.message)
            );
    }, [url, setData])


    const Data = () => {
        switch (requestState) {
            case 'loading':
                return <div>Loading...</div>;
            case 'loaded':
                return <ConstructTable items={data}/>
            default:
                return <div>{requestState}</div>
        }
    }

Although I don't feel this is ideal.

1

There are 1 answers

0
tigerpaw On

Well, I created a sandbox based on your first version. It seems to be working fine. Note: Only Employee and Material button is enabled. Also the table data is different from yours. But it should illustrate the idea.

https://codesandbox.io/s/friendly-wiles-kjpjw?file=/src/Table.js