Where to calculate groups for fluentui detaillist in nextjs - reactjs

I am learning react, and given this simple example of using SWR to fetch some items from an API and showing the items with groups using fluentui DetailedList - I am running into a problem with the groups.
Whenever I click a group in UI to collapse/uncollapse, that seems to trigger a rerender, and then the component will createGroups(data) again which resets the UI again back to original state as the groups object is recalculated.
Where am I supposed to actually store / calculate the groups information of my data? Initial it needs to be created, but from there it seems that it should only needs to be reevaluated whenvere the swr api returns new data - and then i still properly would want to merge in the current state from collapsed groups that the user might have changed in the UI.
Is it because i properly should not use SWR as it refreshes data live - and only do it on page refresh?
const SWR = ({ children, listid, onSuccess }: { children: ((args: SWRResponse<any, any>) => any), listid: string, onSuccess?: any }) => {
const url = `http://localhost:7071/api/Lists/${listid}`;
console.log(url);
const {data,error } = useSWR(url, { fetcher: fetcher, isPaused: () => listid === undefined, onSuccess });
const items = data.value;
const groups = createGroups(data)
return <... DetailsList group={groups} items={items} ... >; // ... left out a few details ...
};

What about adding a state for holding the groups and an useEffect for when data changes and insde the useEffect you should check if the content has changed before updating the groupState.
const hasChanged(data) => {
return data.notEquals(state.data)); // write your own logic for comparing the result
};
useEffect(() => { if (hasChanged(data)) {
setState(prev=> ({ ...prev, group: createGroup(data), data: data });
}}, [data]);
You dont actually need to store the group, you can just hold the data in your state, but the important part is to be able to check if any change actually took place before changing the state.
Another thing worth trying is the compare option in the useSWR hook. So instead of placing the "hasChanged" logic inside an useEffect hook, perhaps it could be in the compare function. Haven't had the chanse to test this myself though.
A third and final option would be to place the creation of groups inside your fetcher. Perhaps the most intuitive solution for this particular case, though I'm not completely sure it will prevent the unnecessary re-renders.
const fetcher = url => axios.get(url).then(res=> {
return {
items: res.data.value,
groups: createGroups(res.data),
};
});
const SWR = ({ children, listid, onSuccess }: { children: ((args: SWRResponse<any, any>) => any), listid: string, onSuccess?: any }) => {
const { data, error } = useSWR(url, fetcher, ...);
return <... DetailsList group={data.groups} items={data.items} ... >; // ... left out a few details ...
};

Related

Make a query based on a state change

I am making a Pokemon team builder, and am trying to use react-query as I am trying to use the cache features to not make so many requests to the open and public PokeAPI.
I have a dropdown that allows a user to select the name of a Pokemon. When a Pokemon is selected, I want the 'Moves' dropdown to populate with a list of that Pokemons moves for selection.
My initial approach was to use a useEffect hook that depended on the Pokemon Dex Number.
useEffect(() => {
if (pokeDexNum) {
// fetch `https://pokeapi.co/api/v2/pokemon/${pokeDexNum}`
}
}, [pokeDexNum])
I have no clue however how to use this with React-Query. What I want to do is when a user selects a Pokemon, a fetch is made to the PokeAPI to fetch that pokemons available moves, and puts them into the options of the 'Moves' dropdown so that they can select moves.
From the code below you can see that when a Pokemon from the 'Pokemon' dropdown is selected, it updates the const [pokeDexNum, setPokeDexNum] = useState(null); state
<Form>
<Form.Group>
<Form.Dropdown
label="Pokemon"
search
selection
clearable
selectOnBlur={false}
value={pokeDexNum}
onChange={(e, data) => setPokeDexNum(data.value)}
options={[
{ text: "Tepig", value: 498 },
{ text: "Oshawott", value: 501 },
{ text: "Snivy", value: 495 },
]}
/>
<Form.Dropdown label="Moves" search selection options={[]} />
</Form.Group>
</Form>
How would I be able to use react query to fetch depending on whether pokeDexNum is updated
Example of query
const { isLoading, error, data } = useQuery('getPokeMoves', () =>
fetch(`https://pokeapi.co/api/v2/pokemon/${pokeDexNum}`).then(res =>
res.json()
)
)
The above query sometimes fetches even when pokeDexNum is null.
As stated in #Arjun's answer, you need to add the pokeDexNum state to the queryKey array. Think of it as a useEffect's dependency. Any time the pokeDexNum gets updated/changes, a refetch will be triggered for that query.
Since pokeDexNum's initial value is null, you don't want the query to fire before the state is updated. To avoid this, you can use the enabled option in useQuery:
const fetchPokemon = (pokeDexNum) => {
return fetch(`https://pokeapi.co/api/v2/pokemon/${pokeDexNum}`).then((res) =>
res.json()
)
}
const { isLoading, error, data } = useQuery(
['getPokeMoves', pokeDexNum],
() => fetchPokemon(pokeDexNum),
{
enabled: Boolean(pokeDexNum)
}
)
Also, it would be a good idea to add some error handling, I imagine you omitted this for brevity.
Solution: You need to add your pokeDexNum to the queryKey of useQuery.
Here is my suggestion,
define a function to call useQuery. adding your pokeDexNum to the queryKey.
const useGetPokeMoves = (pokeDexNum) => {
return useQuery(
["getPokeMoves", pokeDexNum],
() => {
return fetch(`https://pokeapi.co/api/v2/pokemon/${pokeDexNum}`).then(
(res) => res.json()
);
},
{ keepPreviousData: true }
);
};
Then use it in your component,
const { data } = useGetPokeMoves(pokeDexNum);
Whenever your state changes, the queryKey will also change and the query will be fetched.

useEffect does not store state for Firebase data on initial load and only shows up after fast refresh

UPDATE
#Chris's answer below helped me with my issue. Installed the React Firebase Hooks package and set up my code as seen below. Everything is working flawlessly now! :)
const [value] = useCollection(collection(db, `${sessionUserId}`), {
snapshotListenOptions: { includeMetadataChanges: true },
})
useEffect(() => {
if (value) {
const allData = JSON.parse(
JSON.stringify(
value?.docs.map((doc) => ({
id: doc.id as string,
resultData: doc.data() as DocumentData,
}))
)
)
setAllLogs(allData)
}
}, [value])
What am I using in my project? NextAuthjs, Firebase, Recoiljs
I have a useRecoilState hook to save logs that users have created.
const [allLogs, setAllLogs] = useRecoilState(modalAllLogs)
The modalAllLogs looks like this:
export const modalAllLogs = atom<Array<DocumentData>>({
key: 'allLogs',
default: [],
})
Here is the issue. I have a useEffect hook that grabs the data the user has from the database and assigns setAllLogs to that data. Note: I * do not * have the state updating anywhere else.
useEffect(() => {
const docQuery = query(
collection(db, `${sessionUserId}`),
orderBy('timestamp', 'desc')
)
const unsubscribe = onSnapshot(
docQuery,
(snapshot: QuerySnapshot<DocumentData>) => {
const data = JSON.parse(
JSON.stringify(
snapshot.docs.map((doc: QueryDocumentSnapshot<DocumentData>) => ({
id: doc.id as string,
resultData: doc.data() as DocumentData,
}))
)
)
setAllLogs(data)
}
)
return unsubscribe
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [db])
What happens is, on initial render, allLogs returns an empty array, which makes sense, but stays empty until I make any edit within the same file the useEffect hook is in, save it, and NextJS triggers it's fast render, only then does allLogs suddenly display the data properly. I did a console.log(snapshot) and it is empty on every initial render as well.
Whenever I add data using the inputs I have created, Firebase stores all the data properly and correctly. The only issue is when the data is not loaded in on initial render. Other than that, I have no other issues.
I am not sure what I am doing wrong here. Maybe it is the RecoilState that I am utilizing wrong?
I have tried many things from other forums - changing the dependency to allLogs (which gave me an infinite loop), tried using getDocs() instead of onSnapshot(), and etc.
The problem here is, that the useEffect isn't triggered because the "db" constant doesn't change.
Have a look at these custom hooks for Firebase/firestore https://github.com/CSFrequency/react-firebase-hooks/tree/master/firestore
or lift the onSnapshot up to get real-time results. But in the last case the renders won't be triggered so I think it's the best to make use of custom hooks.

React Hook, how to avoid multiple call of api?

I use React hook and AntD table, the table has several pages to show the data from a backend server, now the problem is when users search on pages, the same api called two times, I know why this happened, but I don't know hot to avoid it.
This only happened when user search on other pages(not the first page)
When user search on page, I want the table to show search result from the first page.
the code as below, you can see the dependency of method useEffect:
the search condition
the pageNum and pageSize
When user input the search condition, and click search button, the api was fired(since searchCondition was changed). and then i update pageNum to 1, the api fired again!(since pageNum was changed), how to make the api only called one time? thanks.
useEffect(() => {
getUsers(pageNum, pageSize, searchCondition)
}, [searchCondition, pageNum, pageSize])
and here is the handler for search button.
const listUser = (params: any) => {
setSearchCondition(params)
setPageNum(1)
}
To avoid this, you may combine both of the actions into an action by using useReducer.
Replace useState by useReducer
const [state, dispatch] = useReducer(
(state, action) => ({ ...state, ...action }),
{
searchCondition: initialState,
pageNum: initialState
}
);
Combine your action
const listUser = (params: any) => {
dispatch({
searchCondition: params,
pageNum: 1
});
}
Call API
useEffect(() => {
getUsers(state.pageNum, pageSize, state.searchCondition)
}, [state.searchCondition, state.pageNum, pageSize])

Filter item on item list that is on state using useState and useEffect

I have a list of items on my application and I am trying to create a details page to each one on click. Moreover I am not managing how to do it with useState and useEffect with typescript, I could manage just using componentDidMount and is not my goal here.
On my state, called MetricState, I have "metrics" and I have seen in some places that I should add a "selectedMetric" to my state too. I think this is a weird solution since I just want to be able to call a dispatch with the action of "select metric with id x on the metrics list that is on state".
Here is the codesandbox, when you click on the "details" button you will see that is not printing the name of the selected metric coming from the state: https://codesandbox.io/s/react-listing-forked-6sd99
This is my State:
export type MetricState = {
metrics: IMetric[];
};
My actionCreators.js file that the dispatch calls
export function fetchMetric(catalog, metricId) {
const action = {
type: ACTION_TYPES.CHOOSE_METRIC,
payload: []
};
return fetchMetricCall(action, catalog, metricId);
}
const fetchMetricCall = (action, catalog, metricId) => async (dispatch) => {
dispatch({
type: action.type,
payload: { metrics: [catalog.metrics.filter((x) => x.name === metricId)] } //contains the data to be passed to reducer
});
};
and finally the MetricsDeatail.tsx page where I try to filter the selected item with the id coming from route parametes:
const metricId = props.match.params.metricId;
const catalog = useSelector<RootState, MetricState>(
(state) => state.fetchMetrics
);
const selectedMetric: IMetric = {
name: "",
owner: { team: "" }
};
const dispatch = useDispatch();
useEffect(() => {
dispatch(appReducer.actionCreators.fetchMetric(catalog, metricId));
}, []);
On my state, called MetricState, I have "metrics" and I have seen in some places that I should add a "selectedMetric" to my state too. I think this is a weird solution since I just want to be able to call a dispatch with the action of "select metric with id x on the metrics list that is on state".
I agree with you. The metricId comes from the URL through props so it does not also need to be in the state. You want to have a "single source of truth" for each piece of data.
In order to use this design pattern, your Redux store needs to be able to:
Fetch and store a single Metric based on its id.
Select a select Metric from the store by its id.
I generally find that to be easier with a keyed object state (Record<string, IMetric>), but an array works too.
Your component should look something like this:
const MetricDetailsPage: React.FC<MatchProps> = (props) => {
const metricId = props.match.params.metricId;
// just select the one metric that we want
// it might be undefined initially
const selectedMetric = useSelector<RootState, IMetric | undefined>(
(state) => state.fetchMetrics.metrics.find(metric => metric.name === metricId )
);
const dispatch = useDispatch();
useEffect(() => {
dispatch(appReducer.actionCreators.fetchMetric(metricId));
}, []);
return (
<div>
<Link to={`/`}>Go back to main page</Link>
Here should print the name of the selected metric:{" "}
<strong>{selectedMetric?.name}</strong>
</div>
);
};
export default MetricDetailsPage;
But you should re-work your action creators so that you aren't selecting the whole catalog in the component and then passing it as an argument to the action creator.

Redux: Reducer not mutating state in first attempt

I am working on a Table in React based application using typescript. I am implementing search functionality for the table. There is a huge amount of data that can be displayed inside table so I am performing search, sorting, pagination all at back end.
I have a component for the table which receives data as props from a parent component. I am using react with redux and using sagas, I get the data from back end. I am using redux state to provide data to component. I am using reducers to mutate the state.
The problem I am facing is that when I reload the data, I get the data and using reducer I mutate the state but that is not being displayed at frontend. But when I try second time, it displays the data.
My code for reducer is below.
const dataEnteries: Reducer<any> = (
state = {},
{ type, detail, pagination }: DataResponse
) => {
switch (type) {
case actionTypes.DATA_LOAD_RESPONSE:
if (!detail) {
return {};
}
const data: any[] = [];
const tableData: any[] = [];
detail.forEach((o) => {
tableData.push(o.dataDetail);
Data.push(o)
})
const resultMap = new Map()
resultMap["data"] = data;
resultMap["tableData"] = tableData;
resultMap["pagination"] = pagination;
return resultMap;
default:
return state;
}
};
here is my map state to props function
const mapStateToProps = ({ data }: { data: DataState }): DataProps => ({
data: data.dataEnteries
});
dataEnteries is the innermost property
I am unable to figure out what is going wrong in my case as at second time, things works rightly.
You could try to use spread operator on return.
return ...resultMap
When you update a prop inside an object the state didn't recognise the change then the render will not be called to refresh the component.
Try this, if not work let me know.
For further referente check this : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax

Resources