1/n Use zustand to reduce re-renders

This commit is contained in:
Igor Katson 2023-12-17 19:27:22 +00:00
parent 3dc2e3eace
commit e6ef3ff23f
No known key found for this signature in database
GPG key ID: B4EC22B66D61A3F5
14 changed files with 210 additions and 87 deletions

View file

@ -1,24 +1,23 @@
import { useContext } from "react";
import { TorrentId, ErrorDetails as ApiErrorDetails } from "../api-types";
import { AppContext } from "../context";
import { TorrentsList } from "./TorrentsList";
import { ErrorComponent } from "./ErrorComponent";
import { useTorrentStore } from "../stores/torrentStore";
import { useErrorStore } from "../stores/errorStore";
export const RootContent = (props: {}) => {
let closeableError = useErrorStore((state) => state.closeableError);
let setCloseableError = useErrorStore((state) => state.setCloseableError);
let otherError = useErrorStore((state) => state.otherError);
let torrents = useTorrentStore((state) => state.torrents);
let torrentsLoading = useTorrentStore((state) => state.torrentsLoading);
export const RootContent = (props: {
closeableError: ApiErrorDetails | null;
otherError: ApiErrorDetails | null;
torrents: Array<TorrentId> | null;
torrentsLoading: boolean;
}) => {
let ctx = useContext(AppContext);
return (
<div className="container mx-auto">
<ErrorComponent
error={props.closeableError}
remove={() => ctx.setCloseableError(null)}
error={closeableError}
remove={() => setCloseableError(null)}
/>
<ErrorComponent error={props.otherError} />
<TorrentsList torrents={props.torrents} loading={props.torrentsLoading} />
<ErrorComponent error={otherError} />
<TorrentsList torrents={torrents} loading={torrentsLoading} />
</div>
);
};

View file

@ -1,13 +1,10 @@
import { useContext, useState } from "react";
import { TorrentStats } from "../../api-types";
import {
AppContext,
APIContext,
RefreshTorrentStatsContext,
} from "../../context";
import { APIContext, RefreshTorrentStatsContext } from "../../context";
import { IconButton } from "./IconButton";
import { DeleteTorrentModal } from "../modal/DeleteTorrentModal";
import { FaPause, FaPlay, FaTrash } from "react-icons/fa";
import { useErrorStore } from "../../stores/errorStore";
export const TorrentActions: React.FC<{
id: number;
@ -23,7 +20,8 @@ export const TorrentActions: React.FC<{
const canPause = state == "live";
const canUnpause = state == "paused" || state == "error";
const ctx = useContext(AppContext);
const setCloseableError = useErrorStore((state) => state.setCloseableError);
const API = useContext(APIContext);
const unpause = () => {
@ -34,7 +32,7 @@ export const TorrentActions: React.FC<{
refreshCtx.refresh();
},
(e) => {
ctx.setCloseableError({
setCloseableError({
text: `Error starting torrent id=${id}`,
details: e,
});
@ -51,7 +49,7 @@ export const TorrentActions: React.FC<{
refreshCtx.refresh();
},
(e) => {
ctx.setCloseableError({
setCloseableError({
text: `Error pausing torrent id=${id}`,
details: e,
});

View file

@ -1,5 +1,5 @@
import { useContext, useState } from "react";
import { AppContext, APIContext } from "../../context";
import { APIContext } from "../../context";
import { ErrorWithLabel } from "../../rqbit-web";
import { ErrorComponent } from "../ErrorComponent";
import { Spinner } from "../Spinner";
@ -7,6 +7,7 @@ import { Modal } from "./Modal";
import { ModalBody } from "./ModalBody";
import { ModalFooter } from "./ModalFooter";
import { Button } from "../buttons/Button";
import { useTorrentStore } from "../../stores/torrentStore";
export const DeleteTorrentModal: React.FC<{
id: number;
@ -20,8 +21,8 @@ export const DeleteTorrentModal: React.FC<{
const [error, setError] = useState<ErrorWithLabel | null>(null);
const [deleting, setDeleting] = useState(false);
const ctx = useContext(AppContext);
const API = useContext(APIContext);
const refreshTorrents = useTorrentStore((state) => state.refreshTorrents);
const close = () => {
setDeleteFiles(false);
@ -37,7 +38,7 @@ export const DeleteTorrentModal: React.FC<{
call(id)
.then(() => {
ctx.refreshTorrents();
refreshTorrents();
close();
})
.catch((e) => {

View file

@ -1,6 +1,6 @@
import { useCallback, useContext, useEffect, useState } from "react";
import { AddTorrentResponse, AddTorrentOptions } from "../../api-types";
import { AppContext, APIContext } from "../../context";
import { APIContext } from "../../context";
import { ErrorComponent } from "../ErrorComponent";
import { formatBytes } from "../../helper/formatBytes";
import { ErrorWithLabel } from "../../rqbit-web";
@ -14,6 +14,7 @@ import { Fieldset } from "../forms/Fieldset";
import { FormInput } from "../forms/FormInput";
import { Form } from "../forms/Form";
import { FileListInput } from "../FileListInput";
import { useTorrentStore } from "../../stores/torrentStore";
export const FileSelectionModal = (props: {
onHide: () => void;
@ -35,7 +36,7 @@ export const FileSelectionModal = (props: {
const [uploadError, setUploadError] = useState<ErrorWithLabel | null>(null);
const [unpopularTorrent, setUnpopularTorrent] = useState(false);
const [outputFolder, setOutputFolder] = useState<string>("");
const ctx = useContext(AppContext);
const refreshTorrents = useTorrentStore((state) => state.refreshTorrents);
const API = useContext(APIContext);
useEffect(() => {
@ -77,7 +78,7 @@ export const FileSelectionModal = (props: {
.then(
() => {
onHide();
ctx.refreshTorrents();
refreshTorrents();
},
(e) => {
setUploadError({ text: "Error starting torrent", details: e });

View file

@ -31,8 +31,4 @@ export const APIContext = createContext<RqbitAPI>({
return null;
},
});
export const AppContext = createContext<ContextType>({
setCloseableError: (_) => {},
refreshTorrents: () => {},
});
export const RefreshTorrentStatsContext = createContext({ refresh: () => {} });

View file

@ -1,6 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { TorrentId, ErrorDetails as ApiErrorDetails } from "./api-types";
import { AppContext, APIContext } from "./context";
import { ErrorDetails as ApiErrorDetails } from "./api-types";
import { APIContext } from "./context";
import { RootContent } from "./components/RootContent";
import { customSetInterval } from "./helper/customSetInterval";
import { IconButton } from "./components/buttons/IconButton";
@ -8,6 +8,8 @@ import { BsBodyText, BsMoon } from "react-icons/bs";
import { LogStreamModal } from "./components/modal/LogStreamModal";
import { Header } from "./components/Header";
import { DarkMode } from "./helper/darkMode";
import { useTorrentStore } from "./stores/torrentStore";
import { useErrorStore } from "./stores/errorStore";
export interface ErrorWithLabel {
text: string;
@ -23,17 +25,20 @@ export const RqbitWebUI = (props: {
title: string;
menuButtons?: JSX.Element[];
}) => {
const [closeableError, setCloseableError] = useState<ErrorWithLabel | null>(
null
);
const [otherError, setOtherError] = useState<ErrorWithLabel | null>(null);
const [torrents, setTorrents] = useState<Array<TorrentId> | null>(null);
const [torrentsLoading, setTorrentsLoading] = useState(false);
let [logsOpened, setLogsOpened] = useState<boolean>(false);
const setCloseableError = useErrorStore((state) => state.setCloseableError);
const setOtherError = useErrorStore((state) => state.setOtherError);
const API = useContext(APIContext);
const setTorrents = useTorrentStore((state) => state.setTorrents);
const setTorrentsLoading = useTorrentStore(
(state) => state.setTorrentsLoading
);
const setRefreshTorrents = useTorrentStore(
(state) => state.setRefreshTorrents
);
const refreshTorrents = async () => {
setTorrentsLoading(true);
let torrents = await API.listTorrents().finally(() =>
@ -41,6 +46,7 @@ export const RqbitWebUI = (props: {
);
setTorrents(torrents.torrents);
};
setRefreshTorrents(refreshTorrents);
useEffect(() => {
return customSetInterval(
@ -66,35 +72,25 @@ export const RqbitWebUI = (props: {
};
return (
<AppContext.Provider value={context}>
<div className="dark:bg-gray-900 dark:text-gray-200 min-h-screen">
<Header title={props.title} />
<div className="relative">
{/* Menu buttons */}
<div className="absolute top-0 start-0 pl-2 z-10">
{props.menuButtons &&
props.menuButtons.map((b, i) => <span key={i}>{b}</span>)}
<IconButton onClick={() => setLogsOpened(true)}>
<BsBodyText />
</IconButton>
<IconButton onClick={DarkMode.toggle}>
<BsMoon />
</IconButton>
</div>
<RootContent
closeableError={closeableError}
otherError={otherError}
torrents={torrents}
torrentsLoading={torrentsLoading}
/>
<div className="dark:bg-gray-900 dark:text-gray-200 min-h-screen">
<Header title={props.title} />
<div className="relative">
{/* Menu buttons */}
<div className="absolute top-0 start-0 pl-2 z-10">
{props.menuButtons &&
props.menuButtons.map((b, i) => <span key={i}>{b}</span>)}
<IconButton onClick={() => setLogsOpened(true)}>
<BsBodyText />
</IconButton>
<IconButton onClick={DarkMode.toggle}>
<BsMoon />
</IconButton>
</div>
<LogStreamModal
show={logsOpened}
onClose={() => setLogsOpened(false)}
/>
<RootContent />
</div>
</AppContext.Provider>
<LogStreamModal show={logsOpened} onClose={() => setLogsOpened(false)} />
</div>
);
};

View file

@ -0,0 +1,21 @@
import { create } from "zustand";
import { ErrorDetails } from "../api-types";
export interface ErrorWithLabel {
text: string;
details?: ErrorDetails;
}
export const useErrorStore = create<{
closeableError: ErrorWithLabel | null;
setCloseableError: (error: ErrorWithLabel | null) => void;
otherError: ErrorWithLabel | null;
setOtherError: (error: ErrorWithLabel | null) => void;
}>((set) => ({
closeableError: null,
setCloseableError: (closeableError) => set(() => ({ closeableError })),
otherError: null,
setOtherError: (otherError) => set(() => ({ otherError })),
}));

View file

@ -0,0 +1,22 @@
import { create } from "zustand";
import { TorrentId } from "../api-types";
export interface TorrentStore {
torrents: Array<TorrentId> | null;
setTorrents: (torrents: Array<TorrentId>) => void;
torrentsLoading: boolean;
setTorrentsLoading: (loading: boolean) => void;
refreshTorrents: () => void;
setRefreshTorrents: (callback: () => void) => void;
}
export const useTorrentStore = create<TorrentStore>((set) => ({
torrents: null,
torrentsLoading: false,
setTorrentsLoading: (loading: boolean) => set({ torrentsLoading: loading }),
setTorrents: (torrents) => set({ torrents }),
refreshTorrents: () => {},
setRefreshTorrents: (callback) => set({ refreshTorrents: callback }),
}));