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.
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.