Inside useEffect is never run in react-redux application - reactjs

I have the following component. I did debugging. The function inside useEffect never get called. The code reaches to useEffect but then does not enter inside, and therefore does not fetch records from the database. Any ideas why this is happening?
import * as React from 'react';
import { useEffect } from 'react';
import { connect } from 'react-redux';
import { FetchAssignmentData } from './AssignmentDataOperations'
const AssignmentComprehensive = (props) => {
useEffect(() => {
if (props.loading != true)
props.fetchAssignment(props.match.params.id);
}, []);
if (props.loading) {
return <div>Loading...</div>;
}
if (props.error) {
return (<div>{props.error}...</div>)
}
//these are always null
const assignmentId = props.assignmentIds[0];
const assignment = props.assignments[assignmentId];
return (
//this throws error since the values are never fetched from db
<div>{props.assignments[props.assignmentIds[0]].title}</div>
);
}
const mapStateToProps = state => ({
assignmentIds: state.assignmentReducer.assignmentIds,
assignments: state.assignmentReducer.assignments,
submissions: state.assignmentReducer.submissions,
rubric: state.assignmentReducer.rubric,
loading: state.assignmentReducer.loading,
error: state.assignmentReducer.error
})
const mapDispatchToProps = dispatch => {
return { fetchAssignment: (id) => dispatch(FetchAssignmentData(id)) };
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(AssignmentComprehensive);

Because of useEffect second argument:
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.
So it runs only once (when the props.loading istrue) and never again.
You seem to have 3 dependencies:
useEffect(() => {
...
}, [props.loading, props.fetchAssignment, props.match.params.id])
See also: react-hooks/exhaustive-deps eslint rule.

Related

Stale custom hook state property on callback

I have a custom hook in my React app that exposes a function (hookFn) to calculate a value. Once the value has been updated (state change, triggering useEffect), the hook alerts the app via a callback function. Here's the issue: in my callback function, I want to be able to access the value via hook.value, but it seems to be stale! Even though I know the value state has been updated!
Codesandbox: https://codesandbox.io/s/stoic-payne-bwp6j5?file=/src/App.js:0-910
import { useEffect, useRef, useState } from "react";
export default function App() {
const hook = useCustomHook();
useEffect(() => {
hook.hookFn(hookCallback);
}, []);
function hookCallback(value) {
console.log({givenValue: value, hookValue: hook.value});
}
return "See console for output";
}
function useCustomHook() {
const callbackRef = useRef(null);
const [value, setValue] = useState("initial value");
useEffect(() => {
if (callbackRef.current) {
callbackRef.current(value);
}
}, [value]);
function hookFn(callbackFn) {
callbackRef.current = callbackFn;
setValue("value set in hookFn");
}
return { hookFn, value };
}
FYI: in my actual app, the hook is for searching, which may call the callback function multiple times as more search results become available.
Is there any way to ensure hook.value will be valid? Or is it bad practice for a hook to expose a state variable in general?
It turns out hook.value is stale because hook is stale when I access it from hookCallback. Each time there is a state change within my custom hook, useCustomHook will generate a new object.
The complex solution, then, is to to create a ref for hook and keep it up to date in useEffect. But then I have to make sure I wait for that useEffect to run before accessing hookRef.current.value... Here's my attempt to make this work: https://codesandbox.io/s/dazzling-shirley-0r7k47?file=/src/App.js
However, a better solution: don't mix React states and manual callbacks. Instead, just watch for state changes in a useEffect, like so:
import { useEffect, useState } from "react";
export default function App() {
const hook = useCustomHook();
useEffect(() => {
hook.hookFn();
}, []);
useEffect(() => {
if (hook.value) console.log({ hookValue: hook.value });
}, [hook.value]);
return "See console for output";
}
function useCustomHook() {
const [value, setValue] = useState("initial value");
function hookFn(callbackFn) {
setValue("value set in hookFn");
}
return { hookFn, value };
}
Notice the code is simplified, and there's no need for concern about states being out-of-sync.
I think you have pretty much answered you own question. Alternatively, you could pass your callback function as input to your custom hook.
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const hook = useCustomHook(hookCallback);
useEffect(() => {
hook.setNewValue();
},[])
function hookCallback(value) {
console.log({
givenValue: value,
hookValue: hook.value, // Why is this stale??
areIdentical: value === hook.value // Should be true!!!
});
}
return <h1>See console for output</h1>;
}
function useCustomHook(callback) {
const [value, setValue] = useState("initial value");
useEffect(() => {
callback(value);
}, [value]);
function setNewValue(callbackFn) {
setValue("value set in hookFn");
setTimeout(() => {
setValue("value set in setTimeout");
}, 100);
}
return { setNewValue, value };
}

React context provider not setting value on page load/refresh

I have the following hook for Pusher and I use it to share one instance across the application.
import React, { useContext, useEffect, useRef } from "react";
import Pusher from "pusher-js";
const PusherContext = React.createContext<Pusher | undefined>(undefined);
export const usePusher = () => useContext(PusherContext)
export const PusherProvider: React.FC = (props) => {
const pusherRef = useRef<Pusher>();
useEffect(() => {
pusherRef.current = new Pusher(PUSHER_APP_KEY, {
cluster: 'eu'
})
return () => pusherRef.current?.disconnect()
}, [pusherRef]);
return (
<PusherContext.Provider value={pusherRef.current}>
{props.children}
</PusherContext.Provider>
)
}
The problem is that the provider always has an undefined value on page refresh/load. But when I trigger a re-render the value is correctly set. I would like to have the instance without the need of re-rendering.
Why is this happening?
I believe you can use the next construction:
export const PusherProvider = (props) => {
const pusher = useMemo(() => new Pusher(APP_PUSHER_KEY, { cluster: 'eu' }), [])
useEffect(() => () => pusher.disconnect(), [pusher])
return <PusherContext.Provider value={pusher}>{props.children}</PusherContext.Provider>
}
I have solved this issue by following way.
If you set State/const data inside "useEffect" will not work. as that will not run when page refresh but the state declaration those are outside the "useEffect" will run. Hence it will reset default values.
So I resolved by setting the state/const value outside of "useEffect" and done.

What is causing Error: Too many re-renders in this code?

Can anybody please look at the following ReactJS component and tell what is causing it to return the error:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
import React, { useState, useEffect } from 'react';
function Lab() {
const [questions, setQuestions] = useState([]);
const addQuestion = (question) => {
let q = [...questions];
q.push(question);
setQuestions(q);
}
addQuestion('What is your name?');
addQuestion('Where do you belong?');
return (
<div>
{
questions.map( q => <div>{q}</div>)
}
</div>
);
}
export default Lab;
Edit:
I can use some default values in useState([]), but that would make my code much messy because the data structure is quite complicated. That's why I want to push default values from within a helper function. Isn't it possible this way?
PROBLEM
Lab function is executed.
addQuestion is executed which triggers a re-render
Re-render triggers another execution of addQuestion
and thereby causes an infinite loop of re-renders and execution of addQuestion.
SOLUTION
add your default question as the default state in your Lab component.
import React, { useState, useEffect } from 'react';
const defaultQuestions = [
'What is your name?', 'What is your name?'
]
function Lab() {
const [questions, setQuestions] = useState(defaultQuestions);
const addQuestion = (question) => {
let q = [...questions];
q.push(question);
setQuestions(q);
}
return (
<div>
{
questions.map( q => <div>{q}</div>)
}
</div>
);
}
export default Lab;
Whenever the state update, the whole function component will re-run again.
Therefore, in your code, when the questions variable is updated, the execution of the addQuestion function will be called again, and the function itself will update the state again, and that causes the infinite loop.
To prevent this kind of situation, it's better to let an event trigger the function.
Edit
If adding some default values is your main purpose, this is how you can do it:
import React, { useState, useEffect, useRef } from 'react';
function Lab() {
const isDefaultValueLoaded = useRef(false);
const [questions, setQuestions] = useState([]);
const addQuestion = (question) => {
let q = [...questions];
q.push(question);
setQuestions(q);
}
// To set the default values:
// use an useEffect hook to load the values when the component just mount.
useEffect(() => {
// to prevent the infinite loop,
// use a ref object as a flag to make sure the function will only run once.
if (isDefaultValueLoaded.current === false) {
loadDefaultValue();
isDefaultValueLoaded.current = true;
}
}, []);
// by writing the code above, you can now seperate the logic into an "helper function" as you mentioned.
const loadDefaultValue = () => {
addQuestion('What is your name?');
addQuestion('Where do you belong?');
}
return (
<div>
{
questions.map(q => <div>{q}</div>)
}
</div>
);
}
export default Lab;

Solve the react-hooks/exhaustive-deps when updating redux in useEffect

Can you help me solve useEffect riddles?
I want to satisfy the linter react-hooks/exhaustive-deps rule but also have a component which does not re-render all the time. My component should aggregate data if it is loading for the first time. This data is fetched from redux, then the summation is done and the data is stored back to redux.
As explained in this question, I could move the aggregation to the component where the data is added, but this would force me to update the arrays more often, once either stocks or transaction is added and this even I never view the aggregate.
This question also points to a post by Dan Abramov not to disable the linter rule. Which let me came to the conclusion just leaving something out in the useEffect array is not the way to go — as it is the case in my example.
Now the question is how to solve this?
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { ITransactionArray } from "../../../store/account/types";
import { IStockArray, STOCKS_PUT } from "../../../store/portfolio/types";
import { useTypedSelector } from "../../../store/rootReducer";
import { AppDispatch } from "../../../store/store";
const aggregateStockQuantities = (
stocks: IStockArray,
transactions: ITransactionArray
): IStockArray => {
console.log("run Value/aggregateStockQuantities");
const newStocks = [...stocks];
newStocks.forEach((s) => {
s.quantity = 0;
return s;
});
transactions.forEach((t) => {
newStocks.forEach((s) => {
if (s.quantity !== undefined && t.isin === s.isin) {
s.quantity += t.quantity;
}
return s;
});
});
return newStocks;
};
const Test: React.FC = () => {
const dispatch: AppDispatch = useDispatch();
const { stocks } = useTypedSelector((state) => state.portfolio);
const { transactions } = useTypedSelector((state) => state.account);
const stockQuantities = aggregateStockQuantities(stocks, transactions);
useEffect(() => {
dispatch({
type: STOCKS_PUT,
payload: stockQuantities,
});
}, [dispatch]);
// only works if I leave out stockQuantities
// but then warning: React Hook useEffect has a missing dependency: 'stockQuantities'. Either include it or remove the dependency array react-hooks/exhaustive-deps
// Render
}
Update #1: Had to change the aggregateStockQuantities so quantity is null at the start.
I would advice making a selector that only selects aggregateStockQuantities from the redux state.
And move the const stockQuantities = aggregateStockQuantities(stocks, transactions); inside the useEffect hook.

What is the best way to use redux action in useEffect?

I have a React Component like shown bellow (some parts are ommited) and I'm using redux for state management. The getRecentSheets action contains an AJAX request and dispatches the response to redux which updates state.sheets.recentSheets with the response's data.
All this works as expected, but on building it throws warning about useEffect has a missing dependency: 'getRecentSheets'. But if I add getRecentSheets to useEffect's dependency array it starts to rerun indefinitely and thus freezes the app.
I've read React documentation about the useEffect hook https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies but it doesn't provide a good example for such usecase. I suppose it is something with useCallback or react-redux useDispatch, but without examples I'm not sure how to implement it.
Can someone please tell me what the most concise and idiomatic way to use redux action in useEffect would be and how to avoid warnings about missing dependencies?
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import SheetList from '../components/sheets/SheetList';
import { getRecentSheets } from '../store/actions/sheetsActions';
const mapStateToProps = (state) => {
return {
recentSheets: state.sheets.recentSheets,
}
}
const mapDispatchToProps = (dispatch) => {
return {
getRecentSheets: () => dispatch(getRecentSheets()),
}
}
const Home = (props) => {
const {recentSheets, getRecentSheets} = props;
useEffect(() => {
getRecentSheets();
}, [])
return <SheetList sheets={ recentSheets } />
};
export default connect(mapStateToProps, mapDispatchToProps) (Home);
After all, it seems that correct way will be as follows:
// ...
import { useDispatch } from 'react-redux';
import { getRecentSheets } from '../store/actions/sheetsActions';
const Home = props => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getRecentSheets());
}, [dispatch])
// ...
};
This way it doesn't complain about getRecentSheets missing in dependencies array. As I understood from reading React doc on hooks that's because it's not defined inside the component. Though I'm new to frontend and I hope I didn't mess something up here.
Passing an empty array in your hook tells React your hook function will not have any dependent values from either props or state.
useEffect(() => {
getRecentSheets();
}, [])
The infinite loop arises when you declare the dispatcher as a dependency on the hook. When the component is initialized, props.recentSheets hasn't been set, and will rerender once you make your AJAX call.
useEffect(() => {
getRecentSheets();
}, [getRecentSheets])
You could try something like this:
const Home = ({recentSheets}) => {
const getRecentSheetsCallback = useCallback(() => {
getRecentSheets();
})
useEffect(() => {
getRecentSheetsCallback();
}, [recentSheets]) // We only run this effect again if recentSheets changes
return <SheetList sheets={ recentSheets } />
};
No matter how many times Homes re-renders, you retain the memoized function to your dispatch call.
Alternatively, you may have encountered find similar patterns utilizing local state and then make your effect "depend" on sheets.
const [sheets, setSheets] = useState(recentSheets)
Hope this helps
I would add a check to see if recentSheets exists or not, using that as my dependency.
useEffect(() => {
if (!recentSheets) getRecentSheets();
}, [recentSheets])

Resources