Initialisieren eines useReducer mit asynchronen Daten in einem Hook
Erstens ein Zusammenhang.
Ich habe eine Funktionskomponente, die Daten asynchron lädt.
const { error, loading, data, getAllData } = useData(); // useData is a custom hook abstracting Context API
if (error) return <Error error={error} />;
if (!data || loading) return <LinearProgress />;
{/* etc .. */}
wird also dataasynchron geladen, was bedeutet, dass es anfangs undefiniert ist, aber wenn das asynchrone Laden beendet ist, erhält es einen echten Wert.
Jetzt muss ich die Daten in einer Tabelle anzeigen und für die Tabelle möchte ich etwas Logik in einen Hook abstrahieren.
Also habe ich am Ende
const { error, loading, data, getAllData } = useData();
const tableCtrl = useTable(data);
if (error) return <Error error={error} />;
if (!data || loading) return <LinearProgress />;
In useTable.tshabe ich
export function useTable(initialItems: TableItem[]): TableCtrl {
const [state, dispatch] = React.useReducer(reducer, {
items: initialItems,
selectedItems: [],
allSelected: false
});
return {
...state,
deleteItem: (itemToDelete: TableItem) =>
dispatch({ type: 'delete_item', item: itemToDelete }),
selectAll: () => dispatch({ type: 'select_all' }),
selectNone: () => dispatch({ type: 'select_none' }),
select: (item: TableItem) => dispatch({ type: 'select', item }),
deselect: (item: TableItem) => dispatch({ type: 'deselect', item })
};
}
Jetzt bin ich mir ziemlich sicher, dass Sie das Problem erkennen können, aber das, mit initialDatadem der Hook das erstellt, useReducerist undefinedund es wird nie wirklich aktualisiert. Wenn also die realen Daten "ankommen", wird der Status des Hooks nicht aktualisiert.
Wie könnte ich das beheben?
Antworten
Das ist eine interessante Frage. Da hook of React.useReducernur einmal den Anfangswert annimmt, wird der Status selbst beibehalten, ohne dass der Wert erneut festgelegt wird.
Die Idee ist also ganz einfach. Sie können Ihren Status einfach über die dispatchFunktion, die Sie bei jeder dataÄnderung exportiert haben, erneut festlegen . Hier ist der Ideenausschnitt:
Erstellen wir eine exportierte Funktion, um Ihren Status zu aktualisieren useTable.ts:
return {
setData: data => dispatch(/* This is where you set your state again */)
}
Rufen setDataSie jetzt rechts Ihre datageänderten in Ihrer Hauptdatei auf:
const { setData, ...others } = useTable(data);
React.useEffect(() => {
if (data) {
// This will sync your state with the latest data
setData(data);
}
}, [data])
Am Ende habe ich es ähnlich gemacht wie @ tmhao2005 erwähnt. Meine endgültige Lösung sieht wie folgt aus. Ich wollte dies tun, um die Tabellenlogik (Auswahl, Löschung usw.) von der Präsentationslogik entkoppeln zu können. Damit ich eine generische Tabellenkomponente erstellen kann (im folgenden Beispiel nicht dargestellt)
const rootElement = document.getElementById("root");
const {
Table,
TableBody,
TableContainer,
TableHead,
TableCell,
TableRow,
Link
} = MaterialUI;
// to mimic async loading of data
function useData() {
const [data, setData] = React.useState([]);
React.useEffect(() => {
setTimeout(() => setData([1, 2, 3]), 0);
}, []);
return {
data,
deleteByIndex: (idx) => {
// some random function to mutate the state
setData(data.filter((_, index) => idx !== index));
}
};
}
function reducer(state, action) {
switch (action.type) {
case "init":
return { ...state, items: action.items };
default:
throw new Error("unknown action");
}
}
function useTable(initialItems) {
const isFunctionCreator = React.useMemo(
() => typeof initialItems === "function",
[initialItems]
);
const [state, dispatch] = React.useReducer(reducer, {
items: isFunctionCreator ? null : initialItems,
selectedItems: [],
allSelected: false
});
const data = isFunctionCreator ? initialItems() : null;
React.useEffect(() => {
if (isFunctionCreator && data) {
dispatch({ type: "init", items: data });
}
}, [isFunctionCreator, data]);
return {
...state
};
}
const App = () => {
const { data, deleteByIndex } = useData();
const ctrl = useTable(() => data);
if (!data) return <div>Loading</div>;
return (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Item</TableCell>
<TableCell align="center" size="small"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{ctrl.items &&
ctrl.items.map((item, idx) => (
<TableRow key={item}>
<TableCell>{item}</TableCell>
<TableCell align="center" size="small">
<Link onClick={() => deleteByIndex(idx)}>Delete</Link>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}
// React bootstrap
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.production.min.js"></script>
<div id="root"></div>