I am learning React for the first time and just shipped my first app.
The app is mounted on a single page in a node/express dashboard app which uses server-side templates. My plan is to gradually extend the React app to replace additional templates within the dashboard.
Everything works, but I have two issues:
- There is already quite a lot of state, and the code is filled with props being passed down to components. I feel like this would make the code unmaintainable as the app grows.
- The code for communicating with the back-end API is not very declarative. I am manually indicating when to refetch from the API, to prevent double fetching on initial load.
I am looking for advice on best practices in React to handle these two issues. The code showing both issues is provided below.
Questions
- What would be the best way for me to manage state in this app as the complexity grows? Would it for example be:
- Create objects for the useState hook instead of primitives and pass those down?
- Use component composition to reduce the prop drilling?
- Use React Context?
- Use Redux?
- Is there a more declarative way of achieving what I am currently doing sync the state with the API?
I am manually disabling fetching initially because the initial query parameters are included in the first object returned from the API but they are not included in the url. So I want to build them when initialising the app without refetching.
Code and stack
I included the main part of the code for the app below, as well as the file tree for the project to give an idea of the structure.
Stack details:
- TypeScript
- Vite.js for dev-server and builds
- SWR for data-fetching
- Express.js backend with cookie-session for sessions and persistent query parameters
/src/Feedback.txs - holds the state and fetches data accordingly
import { useEffect, useState } from "react";
import useSWR from "swr";
import { getActiveTeamID, getTimescaleValue } from "./lib/util";
import Header from "./components/header/Header";
import Body from "./components/feedback/Body";
export default function Feedback() {
// API
const [queryParams, setQueryParams] = useState("");
const { data, error } = useSWR(`/client/feedback${queryParams}`);
// Local state
const [title, setTitle] = useState("Feedback");
const [translate, setTranslate] = useState(true);
const [page, setPage] = useState(1);
const [lng, setLng] = useState("is");
const [teams, setTeams] = useState([]);
const [activeTeamID, setActiveTeamID] = useState(0);
const [showChildren, setShowChildren] = useState(true);
const [timescale, setTimescale] = useState("last-3");
const [searchTerm, setSearchTerm] = useState("");
const [isDummy, setIsDummy] = useState(false);
// Trigger for refetching
const [refetch, setRefetch] = useState(false);
const [pageChange, setPageChange] = useState(false);
const isLoading = !error && !data;
// Determine initial state
useEffect(() => {
if (data) {
setLng(data.client.lng);
setTitle(data.client.title);
setTeams(data.filters.teams);
setIsDummy(data.filters.dummy);
setActiveTeamID(getActiveTeamID(data.filters.teams));
setShowChildren(data.filters.children);
setTimescale(getTimescaleValue(data.filters.timescale.selected));
document.title = `${data.client.title}`;
}
}, [data]);
// Refetch from API
useEffect(() => {
if (refetch) {
const pageNoToUse = pageChange ? page : 1;
setQueryParams(
`?team=${activeTeamID}&children=${showChildren}&view=${timescale}&search=${searchTerm}&page=${pageNoToUse}`
);
}
setPageChange(false);
setRefetch(false);
}, [refetch]);
if (error) {
return <div>{error}</div>;
}
return (
<>
<Header
title={title}
lng={lng}
filters={data?.filters}
translate={translate}
setTranslate={setTranslate}
setQueryParams={setQueryParams}
teams={teams}
setTeams={setTeams}
activeTeamID={activeTeamID}
setActiveTeamID={setActiveTeamID}
showChildren={showChildren}
setShowChildren={setShowChildren}
timescale={timescale}
setTimescale={setTimescale}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
setRefetch={setRefetch}
/>
<Body
feedback={data?.feedback}
lng={lng}
translate={translate}
page={page}
setPage={setPage}
pagination={data?.pagination}
isLoading={isLoading}
setRefetch={setRefetch}
setPageChange={setPageChange}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
isDummy={isDummy}
/>
</>
);
}
/src/main.txs - this is the entry point to the app
import "vite/modulepreload-polyfill";
import React from "react";
import ReactDOM from "react-dom/client";
import Feedback from "./Feedback";
import { SWRConfig } from "swr";
import fetcher from "./lib/util/fetcher";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<SWRConfig
value={{
fetcher: fetcher,
}}
>
<Feedback />
</SWRConfig>
</React.StrictMode>
);
File tree
client
┣ dist
┃ ┣ assets
┃ ┃ ┗ main.b889a7af.js
┃ ┗ manifest.json
┣ node_modules
┃ ┣ ...
┣ src
┃ ┣ components
┃ ┃ ┣ feedback
┃ ┃ ┃ ┣ chat
┃ ┃ ┃ ┃ ┣ CommentEmployee.tsx
┃ ┃ ┃ ┃ ┣ CommentInitial.tsx
┃ ┃ ┃ ┃ ┣ CommentManager.tsx
┃ ┃ ┃ ┃ ┣ Footer.tsx
┃ ┃ ┃ ┃ ┣ Panel.tsx
┃ ┃ ┃ ┃ ┗ PanelContent.tsx
┃ ┃ ┃ ┣ Body.tsx
┃ ┃ ┃ ┣ Card.tsx
┃ ┃ ┃ ┗ Pagination.tsx
┃ ┃ ┣ header
┃ ┃ ┃ ┣ filters
┃ ┃ ┃ ┃ ┣ ActiveButtons.tsx
┃ ┃ ┃ ┃ ┣ ChildrenFilter.tsx
┃ ┃ ┃ ┃ ┣ Filters.tsx
┃ ┃ ┃ ┃ ┗ TeamFilter.tsx
┃ ┃ ┃ ┣ other
┃ ┃ ┃ ┃ ┣ DownloadExcel.tsx
┃ ┃ ┃ ┃ ┣ Search.tsx
┃ ┃ ┃ ┃ ┣ Title.tsx
┃ ┃ ┃ ┃ ┗ Translations.tsx
┃ ┃ ┃ ┣ timescale
┃ ┃ ┃ ┃ ┣ SubmenuRange.tsx
┃ ┃ ┃ ┃ ┣ SubmenuSurvey.tsx
┃ ┃ ┃ ┃ ┗ Timescale.tsx
┃ ┃ ┃ ┗ Header.tsx
┃ ┣ lib
┃ ┃ ┣ util
┃ ┃ ┃ ┣ classnames.ts
┃ ┃ ┃ ┣ fetcher.ts
┃ ┃ ┃ ┗ index.ts
┃ ┃ ┗ translate.ts
┃ ┣ Feedback.tsx
┃ ┣ main.tsx
┃ ┗ vite-env.d.ts
┣ .eslintrc.json
┣ package-lock.json
┣ package.json
┣ tsconfig.json
┣ tsconfig.node.json
┗ vite.config.ts