I have a page with a Table, this table is controlled with other inputs which are put in common through a custom hook, I try to read the values in the hook in the page where the component is but although the values inside the hook are updated they are not read in the page
to clarify
[Page] - contains -> [ table ] - controlled by [inputs in page]
// Due to a complex state, the state of the hook is controlled by a reducer
this is a smaller version of codesandbox reproducing the issue. https://codesandbox.io/p/sandbox/charming-lumiere-vyoewu?file=%2Fsrc%2Freducers%2FpageQueryReducer.ts&selection=%5B%7B%22endColumn%22%3A3%2C%22endLineNumber%22%3A67%2C%22startColumn%22%3A3%2C%22startLineNumber%22%3A67%7D%5D
// Hook
import {
PageQueryActionKind,
pageQueryReducer,
queryInit,
} from "../reducers/pageQueryReducer";
import { useEffect, useReducer } from "react";
const useTable = () => {
const [state, dispatch] = useReducer(pageQueryReducer, queryInit);
useEffect(() => {
console.log("state read by the hook", state);
}, [state]);
const handlePageChange = (page: number) => {
dispatch({
type: PageQueryActionKind.SET_PAGE,
payload: {
page,
per_page: state.perPage,
},
});
};
const handlePerPageChange = (perPage: number) => {
dispatch({
type: PageQueryActionKind.SET_PAGE,
payload: { page: state.page, per_page: perPage },
});
};
const handleSortChange = (column: string, direction: "asc" | "desc" | "") => {
dispatch({
type: PageQueryActionKind.SET_COL_SORT,
payload: {
columnSortItem: column,
columnOrder: direction,
},
});
};
return {
currentPage: state.page,
setCurrentPage: handlePageChange,
entriesPerPage: state.perPage,
setEntriesPerPage: handlePerPageChange,
columnSortDirection: state.columnOrder,
currentSortedColumn: state.columnSortItem,
setColumnSort: handleSortChange,
tableFilters: state.tableFilters,
queryString: state.queryString,
overAllState: state,
};
};
export default useTable;
// Components
const Checkbox = ({ label, onChangeFunc }) => {
return (
<div className="checkbox-wrapper">
<label>
<input type="checkbox" onChange={onChangeFunc} />
<span>{label}</span>
</label>
</div>
);
};
export default Checkbox;
import Checkbox from "./checkbox";
import useTable from "../hooks/useTable";
const Table = () => {
const { setColumnSort } = useTable();
return (
<div>
Test quote and quote table
<Checkbox
label="test"
onChangeFunc={() => setColumnSort("hipothethicalColumn", "asc")}
/>
</div>
);
};
export default Table;
// Reducer
type TableFilters = {
col: string;
order: string;
};
interface State {
page: number;
perPage: number;
tableFilters: TableFilters[];
columnSortItem: string;
columnOrder: "asc" | "desc" | "";
queryString: string;
}
export enum PageQueryActionKind {
SET_PAGE = "set_page",
SET_COL_SORT = "set_col_sort",
SET_FILTER = "set_filter",
SET_ROWS_PER_PAGE = "set_rows_per_page",
RESET = "reset",
}
interface SetPageAction {
type: PageQueryActionKind.SET_PAGE;
payload: {
page: number;
per_page: number;
};
}
interface SetColSortAction {
type: PageQueryActionKind.SET_COL_SORT;
payload: {
columnSortItem: string;
columnOrder: "asc" | "desc" | "";
};
}
interface SetFilterAction {
type: PageQueryActionKind.SET_FILTER;
payload: {
filter: string;
value: string;
};
}
interface SetRowsPerPageAction {
type: PageQueryActionKind.SET_ROWS_PER_PAGE;
payload: number;
}
type Actions =
| SetPageAction
| SetColSortAction
| SetFilterAction
| SetRowsPerPageAction
| { type: PageQueryActionKind.RESET; payload: undefined };
export const queryInit = {
page: 1,
perPage: 25,
tableFilters: [],
columnSortItem: "intervention_code",
columnOrder: "desc",
queryString:
"/afm_interventions?company_id=1&page=1&per_page=25&order_by=intervention_code&order=desc",
};
export const pageQueryReducer = (state: State, action: Actions): State => {
console.log("reducer prev values and action", { action, state });
switch (action.type) {
case PageQueryActionKind.SET_PAGE:
return {
...state,
page: action.payload.page,
perPage: action.payload.per_page,
queryString: state.queryString.replace(
/page=[0-9]+&per_page=[0-9]+/,
`page=${action.payload.page}&per_page=${action.payload.per_page}`
),
};
case PageQueryActionKind.SET_COL_SORT:
return {
...state,
columnSortItem: action.payload.columnSortItem,
columnOrder: action.payload.columnOrder,
queryString: state.queryString.replace(
/order_by=[a-z_]+&order=[a-z]+/,
`order_by=${action.payload.columnSortItem}&order=${action.payload.columnOrder}`
),
};
case PageQueryActionKind.SET_FILTER:
if (
state.tableFilters.find(
(tableFilter) => tableFilter.col === action.payload.filter
)
) {
return {
...state,
tableFilters: state.tableFilters.map((tableFilter) => {
if (tableFilter.col === action.payload.filter) {
return {
...tableFilter,
order: action.payload.value,
};
}
return tableFilter;
}),
};
}
return {
...state,
tableFilters: [
...state.tableFilters,
{ col: action.payload.filter, order: action.payload.value },
],
};
case PageQueryActionKind.SET_ROWS_PER_PAGE:
return {
...state,
perPage: action.payload,
};
case PageQueryActionKind.RESET:
return queryInit;
default:
return state;
}
};
there's no shared state
If I'm looking at it right,
MainPageandTableeach have their own instance ofuseTableand therefore each have their own state, reducer, and dispatch. To see the issue, add auseEffecttoMainPageto see that clicking the checkbox does not cause re-render in main page. Then add one toTableto see that it does change.The hint that the state is separate/disconnected is that
<Table/>is created without any props. The easiest option is to take the hook's return value and pass it to the table as a prop -faux complexity
"Due to a complex state, the state of the hook is controlled by a reducer." The complexity you are experiencing is unfortunately self-induced. First I will mention that
queryStringis derived state and shouldn't be stored as state of it its own. It can always be determined from the state of the other values, so remove that from the state model -The hook is dramatically simplified. No need for a laundry list of action types and custom state handlers.
useStatecan handle all state changes anduseMemocan computequeryString-filters and sorting
The proposed reducer also shows an opportunity to improve filters in a big way. a
Mapofcolumntovalueprovides constant-time read/write, more appropriate than array which requires linear scan -To make the hook more usable, we will expose a custom
setFilter(column, value)function that callssetFiltersbehind the scenes -To set a filter, you can create inputs that set a filter for a specified column -
To display the filtered rows, you simply
.filteryourtableRowsdata to ensure the data matches thefiltersstate,.sort, then.mapthe result to display each row that passes the filters -useContext
If you want to share state across components without sending them through props, consider
createContextanduseContext-Create a
<TableProvider>boundary for all components that should be able to access the shared state -Now all child components simply call
useTableto access the shared state -