How to use RTK Query data for derived state? - reactjs

I am building a small app that tracks progress of mountain summit peak lists for hikers. I am using RTK Query for the first time for the majority of state management. I need to create a piece of derived state, a "list counts" object, which contains key value pairs of peak list IDs and the number of peaks completed on that list. Data returned from two RTK Query hook calls is needed to calculate it and this derived data is needed in multiple components:
const listCounts = {
nh4k: 6,
ne100: 5,
usHigh: 3,
// more data
}
I have two query hooks from RTK Query which work to fetch the necessary data from the backend. My current working solution is a custom hook which calls each hook generated by RTK Query and returns listCounts:
const useListCounts = () => {
const { data: peakLists } = useGetPeakListsQuery();
const { data: logEntries } = useGetLogEntriesQuery();
// logic to create listCounts object
return listCounts;
}
Is this appropriate use of a custom hook? I don't have a ton of experience using custom hooks but most other examples I have seen return some value from a built in React hook such as useState, useReducer etc.
Another solution I have considered is to create a separate function, call each RTK query hook in the component directly and then pass the data to the function in the component:
// in util function file:
const calculateListCounts = (peakLists, logEntries) => {
// logic to create listCounts object
return listCounts;
}
// In component file:
import { calculateListCounts } from "..."
const MyComponent = () => {
const { data: peakLists } = useGetPeakListsQuery();
const { data: logEntries } = useGetLogEntriesQuery();
const listCounts = calculateListCounts(peakLists, logEntries);
// return JSX
}
Is there any reason to prefer either option above, or is there some way to access RTK query cache outside of a React component or custom hook so that I can calculate and return listCounts with a single function call?
I have several other similar situations where some piece of derived state calculated using data returned from RTK Query hooks is needed across multiple components and want to make sure I'm following the correct pattern.

Related

RTK-query: how to have the fixedCacheKey approach also for useLazyQuery?

reading the documentation I saw that mutations can share their result using the fixedCacheKey option:
RTK Query provides an option to share results across mutation hook instances using the fixedCacheKey option. Any useMutation hooks with the same fixedCacheKey string will share results between each other when any of the trigger functions are called. This should be a unique string shared between each mutation hook instance you wish to share results.
I have a GET api call that I need to invoke using the trigger method of the useLazyQuery hook, but I need to exploit its booleans (isSuccess, isError, etc...) in many places. Is it possible to have the same behaviour for the useLazyQuery hooks ?
In RTK-query, queries instances follow this logic in cache (from documentation):
When you perform a query, RTK Query automatically serializes the request parameters and creates an internal queryCacheKey for the request. Any future request that produces the same queryCacheKey will be de-duped against the original, and will share updates if a refetch is trigged on the query from any subscribed component.
Given that the only way, in different components, to get the same cache entry is to pass the same query parameters. If you have many useLazy in many components, those hooks don't point any cache entry until you fire them (with some parameters).
This is what I can understand from the official docs
I ended up implementing a custom hook to handle this scenario
// This is the custom hook that wrap the useLazy logic
const useCustomHook = () => {
const [trigger] = useLazyGetItemQuery();
const result = apiSlice.endpoints.getItem.useQueryState(/* argument if any */);
const handleOnClick = async () => {
// The second argument for the trigger is the preferCacheValue boolean
const { data, error } = await trigger(/* argument if any */, true);
// Here where I will implement the success/error logic ...
}
return { onClick: handleOnClick, result}
}
Setting the preferCacheValue at true forced the RTQK to return the cached value if present for the lazy I'm using. That allow me both to avoid triggering many API calls.
Once implemented the hook above I can do the following:
// Component that will make the API call
function Header() {
const { onClick } = useCustomHook();
return (<Button onClick={onClick}>Click me</Button>);
}
// Component that will use the booleans
function Body() {
const { result: { isFetching } } = useCustomHook()
return (<Spin spinning={isFetching}>
<MyJSX />
</Spin>);
}
With this approach I was able both to centralize the trigger logic (e.g. which parameters I have to pass to my trigger function) and exploiting the booleans of that RTKQ hooks in many components.

React Using Correct Memo for Table Column Mapping

I have a React component that uses HandsOnTable to render a table (not necessarily important to the issue but figured I'd give context).
Every time the component renders the table, it runs a renderer function on every column header to handle formatting. I want to memoize the this function pumping out the columns because tables can have 100+ columns and the renderer/formatter it getting very expensive on the big tables.
const getSettings = () => {
// getSettings only runs once or twice, but getTableColumns will run once for every column
return {
columns: getTableColumns
}
}
const getTableColumns = (index) => {
const column = data[index]
const renderer = () => {
// expensive formatting using the column data
}
return {
renderer
}
}
I'm unsure which to use, memo, useMemo, useCallback; I'm new to the memo situation in React. Also, I've read the React versions of memo aren't like actual memo like in lodash? Here is what I've tried:
columns: useMemo(() => getHotColumns, [props.selectedNode])
This unfortunately throws an error: Invalid hook call. Hooks can only be called inside of the body of a function component.
Every time the user selects a new node on my graph UI I want to rerender my table to handle the new data, aka when props.selectedNode changes.
The quick version:
It sounds to me like what you actually want here is to memoize the output of each renderer invocation. That could looks something like this:
// This is the important bit, we are caching the rendered output for each column
const renderedColumns = useMemo(() => {
const renderer = (columnData) => {
// expensive formatting using the column data
}
return your_column_data.map(renderer);
}, [your_column_data]);
const getTableColumns = (index) => {
// Return the cached value instead of re-calculating
return renderedColumns[index];
};
// This memoization sounds like it is less important, but it doesn't hurt.
const columnSettings = useMemo(() => ({
columns: getTableColumns
}), [getTableColumns]);
I'm unsure which to use, memo, useMemo, useCallback
Quick overview, as I understand it:
memo - Memoizes a React component. This means that it won't rerender unless one of its props change. (normally, if the parent rerenders, so will the child)
useMemo computes and caches a value until one of the values in the specified dependencies array changes, in which case it will recalculate using the provided function.
useCallback afaik this is identical to useMemo, just has a cleaner interface for memoizing functions.

How to use Apollo Client's useQuery and useMutation Hooks with Ant Design Forms

In our web application, we use React with Apollo Graphql. As it is recommended in the apollo docs, we want to use the useQuery and useMutation hooks to communicate with the backend server.
Our app works roughly like this: We import a excel-file, parse it in the frontend, send the resulting javascript objects via graphql to the backend and save them in the mongodb database.
In the frontend, we have some forms containing the excel content. After we make some changes, we can save and overwrite the data in the database. In the end, a new excel can be exported from the database data.
import { Form } from "antd";
export default () => {
const [form] = Form.useForm();
const [exelId, setExcelId] = useState("");
const [saveData] = useMutation(SAVEDATA, {
onCompleted: ({ dataFromDB }) => {
form.setFieldsValue(dataFromDB);
},
});
const { data: _ } = useQuery(GETDATA, {
skip: !exelId,
onCompleted: ({ dataFromDB }) => {
form.setFieldsValue(dataFromDB);
},
});
const onFinish = (formValues) => {
saveData({ variables: { formValues } });
};
return (
<Form form={form} onFinish={onFinish}>
<ExcelUpload saveData={saveData} setExcelId={setExcelId} />
<SomeForms />
<Button type="primary">Save</Button>
</Form>
);
};
The problem with our current code is that it causes unnecessary network traffic: After uploading a excel-file, I start a mutation, which returns the required data and sets an "excelId" state. This trigger then the query which fetches the same data. However, I need this query if i reload the page or want to load different data from the database.
I think there are two ways to solve this:
1. Don't use the useMutation hook for filling the form:
That would be easy to implement as I don't have to distinguish between fetching data after an excel-upload or fetching existing data from the data base. But I still need a mutation AND a query.
2. Don't start a query in the excel-uploading process, as the data is received via the mutation:
I think that is complicated to implement with Apollo Client hooks and not the intended way how to use the useQuery hook.
Is there some kind of best practices, how to use the Apollo Client hooks in similar scenarios?
I thought that using those hooks should simplify things, e. g. using the useQuery hook can serve as state. However, using ant design form we also have this form state, which we have to manually set and read with "form.setFieldsValue()" and "form.getFieldsValue()". Is there a good way to combine those with useQuery?

Access data already fetched with react query in other component

I'm new with React Query, and I have a question, I've being looking on the documentation but I can't find how should I access the data that is already fetched with useQuery() from another component.
I'm fetching const query = useQuery('todos', fetchFunction) from HomeComponent and I want to access that data that is already fetched in the TodosComponent. Is ok to fetch the data again with const query = useQuery('todos', fetchFunction) or is there anything like redux that the data is in something like Store so I can access it from any place ?
;)
It is definitely best to just call useQuery again, because it's the only thing that creates a subscription, so your component will re-render correctly if new data comes in. You can do it imperatively with queryClient.getQueryData('todos'), but it doesn't create a subscription.
Note that useQuery will not always trigger a fetch of the data. You can customize staleTime to tell react-query how long a resource is considered fresh, and as long as it's fresh, data will come from the internal cache only. If you set staleTime: Infinity, there will only be one fetch, and all other invocations will only read from the cache (apart from manual invalidations and garbage collection).
It's also best to extract useQuery calls to a custom hook:
const useTodos = () => useQuery('todos', fetchFunction)
HomeComponent:
const { data } = useTodos()
TodosComponent:
const { data } = useTodos()
You can create custom hook with useQueryClient
import { useQueryClient } from "react-query";
export const useGetFetchQuery = (name) => {
const queryClient = useQueryClient();
return queryClient.getQueryData(name);
};
And in component just write like this
const data = useGetFetchQuery("todos");
You can use options { refetchOnMount: false } to use it in many other components, it will render only once and make only one api call.

Use external data in XState FSM

I'm trying to shim XState into an existing state management system (in a React app) and I'm trying to figure out how to represent the state that is already captured in the legacy state management without duplication.
import {useLegacyState} from 'legacy-state-system'
import {useMachine} from '#xstate/react'
import {MyMachine} from '../machine'
const MyComponent = () => {
const [data, setData] = useLegacyState();
const [state, send] = useMachine(MyMachine)
.....JSX etc....
}
For some of the data there is no overlap, but in at least one case (selecting an item on screen, causes the app to send({type: "SELECT_ITEM", itemId: "xyz"}) and fire setData("XYZ")), both legacy and new systems care about the item. XState is being used for UI State Management but the legacy system has side effects that depends on its internal state, so I can't only have data in XState.
My understanding of XState is that I should represent itemId as continuous data in XState's context, but that duplicates the data and I'm concerned that presents a maintenance issue since all developers forever will need to know to update both simultaneously. Is there a way for XState Context to take a value from a runtime-evaluated function? I know that there's assign if I want to push values into Context but that's susceptible to the same maintenance issue so I'm looking for a way to pull values from legacy-state-manager when I call state.context.itemId.
What about wrapping useMachine and using that instead?
import { useMachine as useXStateMachine } from '#xstate/react'
export const useMachine = (machine, options) => {
const [data, setData] = useLegacyState();
const [state, send] = useXStateMachine(machine)
const context = new Proxy({}, {
get: (_, prop) => {
try {
return state.context[prop] || data[prop]
} catch (_) {
return data[prop]
}
}
})
return [{...state, context}, send]
}
The view or the react layer gets updated every time the data store changes and renders it. Typically in a MVC architecture, these logics are built into the controller, where multiple data stores are combined and the resulting data is returned to the UI. In a hook based approach like how you have used, you an create services, that wraps the datastore logics, within it and return only the data required at the UI level.
import {useCustomService} from './services';
const MyComponent = () => {
const [uiData, updateUI] = useCustomService();
}

Resources