Nothing shows up after setState filling by components - javascript

Client: React, mobx
Server: NodeJS, MongoDB
Short question:
I have an array of elements which fills inside of useEffect function, expected result: each element of array should be rendered, actual result: nothing happens. Render appears only after code changing in VSCode.
Tried: changing .map to .forEach, different variations of spread operator in setState(...[arr]) or even without spread operator, nothing changes.
Info:
Friends.jsx part, contains array state and everything that connected with it, also the fill-up function.
const [requestsFrom, setRequestsFrom] = useState([]) //contains id's (strings) of users that will be found in MongoDB
const [displayRequestsFrom, setDisplayRequestsFrom] = useState([]) //should be filled by elements according to requestsFrom, see below
const getUsersToDisplayInFriendRequestsFrom = () => {
const _arr = [...displayRequestsFrom]
requestsFrom.map(async(f) => {
if (requestsFrom.length === 0) {
console.log(`empty`) //this part of code never executes
return
} else {
const _candidate = await userPage.fetchUserDataLite(f)
_arr.push( //template to render UserModels (below)
{
isRequest: true,
link: '#',
username: _candidate.login,
userId: _candidate._id
}
)
console.log(_arr)
}
})
setDisplayRequestsFrom(_arr)
// console.log(`displayRequestsFrom:`)
console.log(displayRequestsFrom) //at first 0, turns into 3 in the second moment (whole component renders twice, yes)
}
Render template function:
const render = {
requests: () => {
return (
displayRequestsFrom.map((friendCandidate) => {
return (
<FriendModel link={friendCandidate.link} username={friendCandidate.username} userId={friendCandidate.userId}/>
)
})
)
}
}
useEffect:
useEffect(() => {
console.log(`requestsFrom.length === ${requestsFrom.length}`)
if (!requestsFrom.length === 0) {
return
} else if (requestsFrom.length === 0) {
setRequestsFrom(toJS(friend.requests.from))
if (toJS(friend.requests.from).length === 0) {
const _arr = [...requestsFrom]
_arr.push('0')
setRequestsFrom(_arr)
}
}
if (displayRequestsFrom.length < 1 && requestsFrom.length > 0) {
getUsersToDisplayInFriendRequestsFrom()
//displayRequestsFrom and requestsFrom lengths should be same
}
},
[requestsFrom]
)
Part of jsx with rendering:
<div className={styles.Friends}>
<div className={styles['friends-container']}>
{render.requests()}
</div>
</div>
UPD: my console.log outputs in the right order from beginning:
requestsFrom.length === 0
requestsFrom.length === 3
displayRequestsFrom === 0
displayRequestsFrom === 3
As we can see, nor requestsFrom, neither displayRequestsFrom are empty at the end of the component mounting and rendering, the only problem left I can't find out - why even with 3 templates in displayRequestsFrom component doesn't render them, but render if I press forceUpdate button (created it for debug purposes, here it is:)
const [ignored, forceUpdate] = React.useReducer(x => x + 1, 0);
<button onClick={forceUpdate}>force update</button>

PRICIPAL ANSWER
The problem here is that you are executing fetch inside .map method.
This way, you are not waiting for the fetch to finish (see comments)
Wrong Example (with clarification comments)
const getUsersToDisplayInFriendRequestsFrom = () => {
const _arr = [...displayRequestsFrom];
// we are not awating requestsFrom.map() (and we can't as in this example, cause .map is not async and don't return a Promise)
requestsFrom.map(async (f) => {
const _candidate = await userPage.fetchUserDataLite(f)
// This is called after setting the state in the final line :(
_arr.push(
{
isRequest: true,
link: '#',
username: _candidate.login,
userId: _candidate._id
}
)
} )
setDisplayRequestsFrom(_arr) // This line is called before the first fetch resolves.
// The _arr var is still empty at the time of execution of the setter
}
To solve, you need to await for each fetch before updating the state with the new array.
To do this, your entire function has to be async and you need to await inside a for loop.
For example this code became
const getUsersToDisplayInFriendRequestsFrom = async () => { // Note the async keyword here
const _arr = [...displayRequestsFrom]
for (let f of requestsFrom) {
const _candidate = await fetchUserData(f)
_arr.push(
{
isRequest: true,
link: '#',
username: _candidate.login,
userId: _candidate._id
}
)
}
setDisplayRequestsFrom(_arr)
}
You can also execute every fetch in parallel like this
const getUsersToDisplayInFriendRequestsFrom = async () => { // Note the async keyword here
const _arr = [...displayRequestsFrom]
await Promise.all(requestsFrom.map((f) => {
return fetchUserData(f).then(_candidate => {
_arr.push(
{
isRequest: true,
link: '#',
username: _candidate.login,
userId: _candidate._id
}
)
});
}));
setDisplayRequestsFrom(_arr);
}
Other problems
Never Calling the Service
Seems you are mapping on an empty array where you are trying to call your service.
const getUsersToDisplayInFriendRequestsFrom = () => {
const _arr = [...displayRequestsFrom]
/* HERE */ requestsFrom.map(async(f) => {
if (requestsFrom.length === 0) {
return
If the array (requestsFrom) is empty ( as you initialized in the useState([]) ) the function you pass in the map method is never called.
Not sure what you are exactly trying to do, but this should be one of the problems...
Don't use state for rendered components
Also, you shoudn't use state to store rendered components
_arr.push(
<FriendModel key={_candidate.id} isRequest={true} link='#' username={_candidate.login} userId={_candidate._id}/>
)
, instead you should map the data in the template and then render a component for each element in your data-array.
For example:
function MyComponent() {
const [myData, setMyData] = useState([{name: 'a'}, {name: 'b'}])
return (<>
{
myData.map(obj => <Friend friend={obj} />)
}
</>)
}
Not:
function MyComponent() {
const [myDataDisplay, setMyDataDisplay] = useState([
<Friend friend={{name: 'a'}} />,
<Friend friend={{name: 'b'}} />
])
return <>{myDataDisplay}</>
}
Don't use useEffect to initialize your state
I'm wondering why you are setting the requestsFrom value inside the useEffect.
Why aren't you initializing the state of your requestsFrom inside the useState()?
Something like
const [requestsFrom, setRequestsFrom] = useState(toJS(friend.requests.from))
instead of checking the length inside the useEffect and fill it
So that your useEffect can became something like this
useEffect(() => {
if (displayRequestsFrom.length < 1 && requestsFrom.length > 0) {
getUsersToDisplayInFriendRequestsFrom()
}
},
[requestsFrom]
)

Related

React useEffect does not work as expected

useEffect(() => {
const method = methodsToRun[0];
let results = [];
if (method) {
let paramsTypes = method[1].map(param => param[0][2]);
let runAlgo = window.wasm.cwrap(method[0], 'string', paramsTypes); //this is emscripten stuff
let params = method[1].map(param => document.getElementById(param[0][0]).value);
let result = runAlgo(...params); //this runs the emscripten stuff
results.push(...(JSON.parse(result)));
setGlobalState('dataOutput', results);
}
let newMethodsToRun = methodsToRun.slice(1);
if (methodsToRun.length>0) {
setGlobalState('methodsToRun', newMethodsToRun);
}
} , [dataOutput, methodsToRun]);
Hello, I am working on a ReactJS app that uses Webassembly with Emscripten.
I need to run a series of algorithms from Emscripten in my Js and I need to show the results as they come, not altogether at the end. Something like this:
Run first algo
Show results on screen form first algo
Run second algo AFTER the results from the first algo are rendedred on the screen (so that user can keep checking them)
Show results on screen form second algo.....
and so on
I already tried a loop that updates a global state on each iteration with timeouts etc, but no luck, so I tried this approach using useEffect, I tried various things but the results keep coming altogether and not one by one. Any clue?
I tried to mock your example HERE:
import React, { useState, useEffect } from 'react';
import './style.css';
export default function App() {
const [methodsToRun, setMethodsToRun] = useState([method1, method2]);
const [globalState, setGlobalState] = useState([]);
useEffect(() => {
if (methodsToRun.length <= 0) return;
const method = methodsToRun[0];
let results = [];
if (method) {
let paramsTypes = method[1].map((param) => param[0][2]);
let runAlgo = window.wasm.cwrap(method[0], 'string', paramsTypes); //this is emscripten stuff
let params = method[1].map(
(param) => document.getElementById(param[0][0]).value
);
let result = runAlgo(...params); //this runs the emscripten stuff
results.push(...JSON.parse(result));
setGlobalState((global) => [...global, results]);
}
setMethodsToRun((methods) => methods.slice(1));
}, [methodsToRun]);
console.log('METHODS TO RUN', methodsToRun);
console.log('GLOBAL RESULT', globalState);
return (
<div>
<div> Global state 1: {globalState[0] && globalState[0][0]}</div>
<div> Global state 2: {globalState[1] && globalState[1][0]}</div>
<div id="1">DIV 1 </div>
<div id="4">DIV 2</div>
<div id="7">DIV 3</div>
</div>
);
}
const method1 = [
'1param0',
[
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
],
];
const method2 = [
'2param0',
[
['11', '223', '3'],
['44', '55', '66'],
['77', '88', '99'],
],
];
//mock wasm
window.wasm = {
cwrap:
(...args) =>
() =>
JSON.stringify(args),
};
This example assumes your wasm methods are synchronous, your DOM will update and rerender with a new globalState after each method is executed, the cycle is ensured by the fact you are setting a new methods array sliced by one in the state, that will trigger a rerender and a new execution of the useEffect since it has the methodsToRun array in the deps array. Infinite render loop is prevented by the check if (methodsToRun.length <= 0) return; .
The drawback of this approach though is that if your wasms methods are synchronous and heavy, your UI will be frozen until it returns, and that's a terrible user experience for the end user. I'm not sure what kind of task you are trying to perform there, if it can be made async and awaited ( the logic would be the same ) or if you have to move that on a service worker.
So from what you've explained here, I think the issue can be resolved in 2 ways:
1 is with synchronous requests/process.
Assuming you have 2 functions (AlgoX and AlgoY) you want to run one after the other, you can return the expected response. e.g.
return AlgoX();
return AlgoY();
The return keyword will block the running process from continuing until you have result.
Another alternative I will suggest will be to use Self-Invoking-Expression:
useEffect(() => {
(
async function AlgoHandler(){
const method = methodsToRun[0];
let results = [];
if (method) {
let paramsTypes = method[1].map(param => param[0][2]);
let runAlgo = await window.wasm.cwrap(method[0], 'string', paramsTypes); //this is emscripten stuff
let params = method[1].map(param => document.getElementById(param[0][0]).value);
let result = await runAlgo(...params); //this runs the emscripten stuff
results.push(...(JSON.parse(result)));
setGlobalState('dataOutput', results);
}
let newMethodsToRun = methodsToRun.slice(1);
if (methodsToRun.length>0) {
setGlobalState('methodsToRun', newMethodsToRun);
}
}
)();
} , [dataOutput, methodsToRun]);
Notice where I used async and await in the second solution. Let me know if its helpful.
I'm not sure if this answers your question, but I wrote some example code that may help you fulfill your needs. It will need to be tweaked to fit your use case though, as this should be considered pseudo code:
const initialState = {
count: 0,
results: [],
};
function reducer(state, action) {
switch (action.type) {
case 'addResult':
return {
...state,
count: state.count + 1,
results: [...state.results, action.result],
};
default:
return state;
}
}
function Example({ methodsToRun }) {
const [{ count, results }, dispatch] = useReducer(reducer, initialState);
const next = useCallback(async() => {
if(methodsToRun.length > count) {
const result = await methodsToRun[count]();
dispatch({ type: 'addResult', result });
}
}, [count, methodsToRun]);
useEffect(() => {
next();
}, [next]);
return (
<div>
<p>Results:</p>
<ol>
{results.map(result => (
<li key={result/* choose a unique key from result here, do not use index */}>{result}</li>
))}
</ol>
</div>
);
}
So basically, my idea is to create a callback function, which at first run will be the method at index 0 in your array of methods. When the result arrives, it will update state with the result, as well as update the counter.
The updated count will cause useCallback to change to the next method in the array.
When the callback changes, it will trigger the useEffect, and actually call the function. And so on, until there are no more methods to run.
I have to admit I haven't tried to run this code, as I lack the context. But I think it should work.
At the end of the day, this solution looks like is working for me:
useEffect(() => {
(
async function AlgoHandler(){
if (methodsToRun.length <= 0) {
setGlobalState('loadingResults', false);
return;
}
const method = methodsToRun[0];
let paramsTypes = method[1].map((param) => param[0][2]);
let runAlgo = window.wasm.cwrap(method[0], 'string', paramsTypes);
let params = method[1].map(
(param) => document.getElementById(param[0][0]).value
);
let result = await runAlgo(...params);
setGlobalState('dataOutput', (prev) => [...prev, ...JSON.parse(result)]);
await sleep(100);
setGlobalState('methodsToRun', (prev) => prev.filter(m => m != method));
})();}, [methodsToRun]);

React hook useEffect runs continuously forever/infinite loop, making duplicate of data fetching from APi,

im fetching data from APi and doing filtering from severside, i'm using useInfiniteScroll to fetch only limited amount of data on the first page, and with this im doing pagination too...
const [casesList, setCasesList] = useState<CaseModel[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isFetchingData, setIsFetchingData] = useState<boolean>(true);
const { inputValue } = React.useContext(MenuContext);
const debouncedValue = useDebounce(inputValue, 10);
these are my hook, (casesList) in which im saving all my incoming data from APi, input value is the value that im typing in search box for filtering the data, and debouncedValue is my custom hook, so the inputValue first goes to debouncedValue and then my debouncedValue will get the value of my inputValue,
const [pagination, setPagination] = useState<Pagination>({
continuationToken: "",
hasMoreResults: true,
});
const [isFetchingMore, setIsFetchingMore] = useInfiniteScroll();
these are my pagination and useInfiniteScroll() hooks...
so the actual problem that i'm facing is that,
const getDashboardCases = useCallback(
async (continuationToken: string) => {
setIsLoading(true);
let casesPageLimit = CASES_PAGE_LIMIT;
if (casesList.length === 0) {
const table = document.querySelector("#cases-items");
if (table) {
const caseItemHeight = 80;
const heightDifference =
table?.getBoundingClientRect().y > 0
? window.innerHeight - table?.getBoundingClientRect().y
: -1;
casesPageLimit = Math.max(
casesPageLimit,
Math.ceil(heightDifference / caseItemHeight)
);
}
}
const options: GetCasesListOptions = {
continuationToken,
filter: [],
sort: [],
pageLimit: casesPageLimit,
search: [debouncedValue]
};
const data: IData = await dashboardService.getCasesList(options);
setIsFetchingMore(false);
setIsLoading(false);
if (data.result) {
setIsFetchingData(false)
if (data.continuationToken !== undefined) {
const newContinuationToken = data.continuationToken;
setPagination((prevPagination) => ({
...prevPagination,
hasMoreResults: data.hasMoreResults,
continuationToken: newContinuationToken,
}));
} else {
setPagination((prevPagination) => ({
...prevPagination,
hasMoreResults: false,
}));
}
setCasesList((prevCases) => [...prevCases, ...data.result]);
dispatch(setAllowedClassifications(data.options));
}
},
[casesList.length, dashboardService, debouncedValue]
);
this code is fetching the data from the APi and for filtering i created an Object name Options
const options: GetCasesListOptions = {
continuationToken,
filter: [],
sort: [],
pageLimit: casesPageLimit,
search: [debouncedValue]
};
im saving my debouncedValue to the search Array in the Options object and then im using Options object in APi to filter the data
const data: IData = await dashboardService.getCasesList(options);
for example if i have 15 objects in APi, i need to get first 10 objects, and then i scroll down my callback function executes one more time and get the rest of the data...
if (data.result) {
setIsFetchingData(false)
if (data.continuationToken !== undefined) {
const newContinuationToken = data.continuationToken;
setPagination((prevPagination) => ({
...prevPagination,
hasMoreResults: data.hasMoreResults,
continuationToken: newContinuationToken,
}));
} else {
setPagination((prevPagination) => ({
...prevPagination,
hasMoreResults: false,
}));
}
setCasesList((prevCases) => [...prevCases, ...data.result]);
dispatch(setAllowedClassifications(data.options));
}
it's already done there...
now i want that, if i type something in the search box my api should run again to add my search value in the APi and filters the data...
but i'm facing problems doing this...
im calling my usecallback function like this...
useEffect(() => {
if (isFetchingMore && pagination.hasMoreResults) {
getDashboardCases(pagination.continuationToken);
}
}, [
getDashboardCases,
isFetchingMore,
pagination.continuationToken,
pagination.hasMoreResults,
debouncedValue
]);
if isFetchingMore && pagination.hasMoreResults is true, then it executes the function, but if type something in searchbox it is not running my function again...
i also tried to remove the if condition in the useEffect but it started infinite scrolling making duplicates of data, and i get this error...
Encountered two children with the same key, `d77c39f2-2dcd-4c4e-b7ee-1fde07b6583f`. Keys should be unique so that components maintain their identity across updates
so i need to re-run the function if i type something in search box and not get duplicated data back, and also i want to run the if condition that i typed in the useEffect...
please help, Thank you :)

React JS state array empty inside function but items appear in render

I'm writing this react component to render all chats of an user in a chat app.
The conversation list is acquired from a REST end point and set as a state variable.
When a new message arrives through socket, I'm trying to bring the conversation in the list to the top and mark it in a different color.
The code for that looks like the following:
const [conversationsList, setConversationsList] = useState([]);
//When a new message arrives,
const markNewConversation = (message) => {
console.log(conversationsList); //Empty array
let newConversationList = conversationsList.map((conversationElement) => {
if (conversationElement.thread_id === message.thread_id) {
conversationElement.date_created = message.date_created;
conversationElement.new_for.push(user._id);
}
return conversationElement;
});
console.log(newConversationList);
newConversationList = newConversationList.sortBy(function (o) {
return o.date_created;
});
console.log(newConversationList); //Empty as well.
setConversationsList(newConversationList); //Whole screen goes black on this.
};
useEffect(() => {
if (user._id === null) history.push("/");
connectSocket();
socket.on("_messageIn", markNewConversation);
getThreads().then((threads) => {
threads.forEach((thread, index) => {
let allParticipants = thread.thread_participants;
threads[index].other_user = allParticipants.find((participant) => {
return participant._id != user._id;
});
});
setConversationsList(threads);
setIsLoading(false);
});
// returned function will be called on component unmount
return () => {
socket.off("_messageIn", markNewConversation);
};
}, []);
return conversationsList.map((conversation) => {
return(//magically appears inside this div.)
})
The problem is when a new message arrives, the function receives an empty array and the entire screen becomes empty. I'm not even setting the array to empty anywhere. Where am I going wrong?
In this function you're mutating objects, this can cause weird errors.
let newConversationList = conversationsList.map((conversationElement) => {
if (conversationElement.thread_id === message.thread_id) {
conversationElement.date_created = message.date_created;
conversationElement.new_for.push(user._id);
}
return conversationElement;
});
it should look like this:
let newConversationList = conversationsList.map((conversationElement) =>
(conversationElement.thread_id === message.thread_id) ? {
...conversationElement,
date_created: message.date_created,
new_for: [...conversationElement.new_for, user.id]
} : {
...conversationElement,
new_for: [...conversationElement.new_for]
}
There is a concept in javascript of State Mutating and we should never mutate the objects directly. Here you are making the mistake is that when new message comes it replaces whole object with new one and kind of re-initialize it. So spread the state using spread operator before adding new message.
Do like this:
let newConversationList = conversationsList.map((conversationElement) =>
(conversationElement.thread_id === message.thread_id) ? {
...conversationElement, //spread/copy like this before mutating
date_created: message.date_created,
new_for: [...conversationElement.new_for, user.id]
} : {
...conversationElement, //spread/copy like this before mutating
new_for: [...conversationElement.new_for]
}

Creating new array vs modifing the same array in react

Following is the piece of code which is working fine, but I have one doubt regarding - const _detail = detail; code inside a map method. Here you can see that I am iterating over an array and modifying the object and then setting it to setState().
Code Block -
checkInvoiceData = (isUploaded, data) => {
if (isUploaded) {
const { invoiceData } = this.state;
invoiceData.map(invoiceItem => {
if (invoiceItem.number === data.savedNumber) {
invoiceItem.details.map(detail => {
const _detail = detail;
if (_detail.tagNumber === data.tagNumber) {
_detail.id = data.id;
}
return _detail;
});
}
return invoiceItem;
});
state.invoiceData = invoiceData;
}
this.setState(state);
};
Is this approach ok in React world or I should do something like -
const modifiedInvoiceData = invoiceData.map(invoiceItem => {
......
code
......
})
this.setState({invoiceData: modifiedInvoiceData});
What is the pros and cons of each and which scenario do I need to keep in mind while taking either of one approach ?
You cannot mutate state, instead you can do something like this:
checkInvoiceData = (isUploaded, data) => {
if (isUploaded) {
this.setState({
invoiceData: this.state.invoiceData.map(
(invoiceItem) => {
if (invoiceItem.number === data.savedNumber) {
invoiceItem.details.map(
(detail) =>
detail.tagNumber === data.tagNumber
? { ...detail, id: data.id } //copy detail and set id on copy
: detail //no change, return detail
);
}
return invoiceItem;
}
),
});
}
};
Perhaps try something like this:
checkInvoiceData = (isUploaded, data) => {
// Return early
if (!isUploaded) return
const { invoiceData } = this.state;
const updatedInvoices = invoiceData.map(invoiceItem => {
if (invoiceItem.number !== data.savedNumber) return invoiceItem
const details = invoiceItem.details.map(detail => {
if (detail.tagNumber !== data.tagNumber) return detail
return { ...detail, id: data.id };
});
return { ...invoiceItem, details };
});
this.setState({ invoiceData: updatedInvoices });
};
First, I would suggest returning early rather than nesting conditionals.
Second, make sure you're not mutating state directly (eg no this.state = state).
Third, pass the part of state you want to mutate, not the whole state object, to setState.
Fourth, return a new instance of the object so the object reference updates so React can detect the change of values.
I'm not saying this is the best way to do what you want, but it should point you in a better direction.

Determine which dependency array variable caused useEffect hook to fire

Is there an easy way to determine which variable in a useEffect's dependency array triggers a function re-fire?
Simply logging out each variable can be misleading, if a is a function and b is an object they may appear the same when logged but actually be different and causing useEffect fires.
For example:
React.useEffect(() => {
// which variable triggered this re-fire?
console.log('---useEffect---')
}, [a, b, c, d])
My current method has been removing dependency variables one by one until I notice the behavior that causes excessive useEffect calls, but there must be a better way to narrow this down.
I ended up taking a little bit from various answers to make my own hook for this. I wanted the ability to just drop something in place of useEffect for quickly debugging what dependency was triggering useEffect.
const usePrevious = (value, initialValue) => {
const ref = useRef(initialValue);
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const useEffectDebugger = (effectHook, dependencies, dependencyNames = []) => {
const previousDeps = usePrevious(dependencies, []);
const changedDeps = dependencies.reduce((accum, dependency, index) => {
if (dependency !== previousDeps[index]) {
const keyName = dependencyNames[index] || index;
return {
...accum,
[keyName]: {
before: previousDeps[index],
after: dependency
}
};
}
return accum;
}, {});
if (Object.keys(changedDeps).length) {
console.log('[use-effect-debugger] ', changedDeps);
}
useEffect(effectHook, dependencies);
};
Below are two examples. For each example, I assume that dep2 changes from 'foo' to 'bar'. Example 1 shows the output without passing dependencyNames and Example 2 shows an example with dependencyNames.
Example 1
Before:
useEffect(() => {
// useEffect code here...
}, [dep1, dep2])
After:
useEffectDebugger(() => {
// useEffect code here...
}, [dep1, dep2])
Console output:
{
1: {
before: 'foo',
after: 'bar'
}
}
The object key '1' represents the index of the dependency that changed. Here, dep2 changed as it is the 2nd item in the dependency, or index 1.
Example 2
Before:
useEffect(() => {
// useEffect code here...
}, [dep1, dep2])
After:
useEffectDebugger(() => {
// useEffect code here...
}, [dep1, dep2], ['dep1', 'dep2'])
Console output:
{
dep2: {
before: 'foo',
after: 'bar'
}
}
#simbathesailor/use-what-changed works like a charm!
Install with npm/yarn and --dev or --no-save
Add import:
import { useWhatChanged } from '#simbathesailor/use-what-changed';
Call it:
// (guarantee useEffect deps are in sync with useWhatChanged)
let deps = [a, b, c, d]
useWhatChanged(deps, 'a, b, c, d');
useEffect(() => {
// your effect
}, deps);
Creates this nice chart in the console:
There are two common culprits:
Some Object being pass in like this:
// Being used like:
export function App() {
return <MyComponent fetchOptions={{
urlThing: '/foo',
headerThing: 'FOO-BAR'
})
}
export const MyComponent = ({fetchOptions}) => {
const [someData, setSomeData] = useState()
useEffect(() => {
window.fetch(fetchOptions).then((data) => {
setSomeData(data)
})
}, [fetchOptions])
return <div>hello {someData.firstName}</div>
}
The fix in the object case, if you can, break-out a static object outside the component render:
const fetchSomeDataOptions = {
urlThing: '/foo',
headerThing: 'FOO-BAR'
}
export function App() {
return <MyComponent fetchOptions={fetchSomeDataOptions} />
}
You can also wrap in useMemo:
export function App() {
return <MyComponent fetchOptions={
useMemo(
() => {
return {
urlThing: '/foo',
headerThing: 'FOO-BAR',
variableThing: hash(someTimestamp)
}
},
[hash, someTimestamp]
)
} />
}
The same concept applies to functions to an extent, except you can end up with stale closures.
UPDATE
After a little real-world use, I so far like the following solution which borrows some aspects of Retsam's solution:
const compareInputs = (inputKeys, oldInputs, newInputs) => {
inputKeys.forEach(key => {
const oldInput = oldInputs[key];
const newInput = newInputs[key];
if (oldInput !== newInput) {
console.log("change detected", key, "old:", oldInput, "new:", newInput);
}
});
};
const useDependenciesDebugger = inputs => {
const oldInputsRef = useRef(inputs);
const inputValuesArray = Object.values(inputs);
const inputKeysArray = Object.keys(inputs);
useMemo(() => {
const oldInputs = oldInputsRef.current;
compareInputs(inputKeysArray, oldInputs, inputs);
oldInputsRef.current = inputs;
}, inputValuesArray); // eslint-disable-line react-hooks/exhaustive-deps
};
This can then be used by copying a dependency array literal and just changing it to be an object literal:
useDependenciesDebugger({ state1, state2 });
This allows the logging to know the names of the variables without any separate parameter for that purpose.
As far as I know, there's no really easy way to do this out of the box, but you could drop in a custom hook that keeps track of its dependencies and logs which one changed:
// Same arguments as useEffect, but with an optional string for logging purposes
const useEffectDebugger = (func, inputs, prefix = "useEffect") => {
// Using a ref to hold the inputs from the previous run (or same run for initial run
const oldInputsRef = useRef(inputs);
useEffect(() => {
// Get the old inputs
const oldInputs = oldInputsRef.current;
// Compare the old inputs to the current inputs
compareInputs(oldInputs, inputs, prefix)
// Save the current inputs
oldInputsRef.current = inputs;
// Execute wrapped effect
func()
}, inputs);
};
The compareInputs bit could look something like this:
const compareInputs = (oldInputs, newInputs, prefix) => {
// Edge-case: different array lengths
if(oldInputs.length !== newInputs.length) {
// Not helpful to compare item by item, so just output the whole array
console.log(`${prefix} - Inputs have a different length`, oldInputs, newInputs)
console.log("Old inputs:", oldInputs)
console.log("New inputs:", newInputs)
return;
}
// Compare individual items
oldInputs.forEach((oldInput, index) => {
const newInput = newInputs[index];
if(oldInput !== newInput) {
console.log(`${prefix} - The input changed in position ${index}`);
console.log("Old value:", oldInput)
console.log("New value:", newInput)
}
})
}
You could use this like this:
useEffectDebugger(() => {
// which variable triggered this re-fire?
console.log('---useEffect---')
}, [a, b, c, d], 'Effect Name')
And you would get output like:
Effect Name - The input changed in position 2
Old value: "Previous value"
New value: "New value"
There’s another stack overflow thread stating you can use useRef to see a previous value.
https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
The React beta docs suggest these steps:
Log your dependency array with console.log:
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
console.log([todos, tab]);
Right-click on the arrays from different re-renders in the console and select “Store as a global variable” for both of them. It may be important not to compare two sequential ones if you are in strict mode, I'm not sure.
Compare each of the dependencies:
Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
This question was answered with several good and working answers, but I just didn't like the DX of any of them.
so I wrote a library which logs the dependencies that changed in the easiest way to use + added a function to log a deep comparison between 2 objects, so you can know what exactly changed inside your object.
I called it: react-what-changed
The readme has all of the examples you need.
The usage is very straight forward:
npm install react-what-changed --save-dev
import { reactWhatChanged as RWC } from 'react-what-changed';
function MyComponent(props) {
useEffect(() => {
someLogic();
}, RWC([somePrimitive, someArray, someObject]));
}
In this package you will also find 2 useful functions for printing deep comparison (diffs only) between objects. for example:
import { reactWhatDiff as RWD } from 'react-what-changed';
function MyComponent(props) {
useEffect(() => {
someLogic();
}, [somePrimitive, someArray, someObject]);
RWD(someArray);
}

Categories

Resources