How to handle useReducer dispatch on props change? - javascript

At the outset I'd like to say that I'm a React beginner and I'm stuck! And I've no idea how to proceed.
I'll be very happy if someone can either help me get my current code working or suggest an alternate approach.
So the most simple background is that I have a React app running on Electron framework to build a desktop application that reads in a JSON file and performs various tasks which includes displaying trees and graphs from the JSON data. To show the graph I have a multiple Menu components which allow the user to select what they want to see. The current state of the Menus are maintained by a component called the MainPane which uses a custom hook called useMenuState for maintaining the menu state (this hook uses only useState).
The values from useMenuState hook are passed by the MainPane as props to a component called the TreeViewer which uses another custom hook called useTreeState which uses a useReducer hook (to generate current tree using the information provided in the props). The custom hook returns the tree data and dispatcher function provided by the reducer hook.
To be able to show a different tree when user selects a new menu option, I have added a useEffect hook in the TreeViewer component which calls the dispatch function.
So I've added the values from the prop in the dependancy array of the useEffect function.
But the effect function is not running when the props change? As far as I can tell, all props being passed are created as new objects and no object state is mutated.
Here is a simple representation of my code (it has all the bits I talked about).
// CUSTOM HOOK useMenuState
const useMenuState = (props) => {
const [menu1State, updateMenu1] = useState();
const [menu2State, updateMenu2] = useState();
const handleMenuClick = (pMenuIdx, pClickedVal) = {
// generate and update respective menu state value using the jsonObj in props
};
let menuState = {
menu1: menu1State,
menu2: menu2State
};
return { menuState, menuClickHandler:handleMenuClick};
}
// COMPONENT MainPane
const MainPane = (props) => {
// props here gets a object that manages the data in the JSON file
const {menuState, menuClickHandler} = useMenuState({...props});
let treeViewerProps = {
treeType: menuState.menu1,
treeID: menuState.menu2,
jsonObj: props.jsonObj
};
return (<TreeViewer {...treeViewerProps} />);
}
// CUSTOM HOOK useTreeState
const useTreeState = (props) => {
const updaterFunction = (currentTreeState, action) => {
// update tree state as per action and return
}
let initTreeState = {} // build initial state of the tree
const [treeObj, treeUpdateDispatcher] = useReducer(updaterFunction, initTreeState);
let treeStateObj = { treeData: treeObj, treeUpdater: treeUpdateDispatcher };
return treeStateObj;
}
// COMPONENT TreeViewer
const TreeViewer = (props) => {
let treeStateProps = {
jsonObj: props.jsonObj,
treeType: props.treeType,
treeID: props.treeID
}
useEffect(()=>{
treeUpdater({type:'NEW TREE', id: props.treeID})
}, [props.treeType, props.treeID]); // look for changes in the tree type or ID and call the dispatch function
const { treeData, treeUpdater } = useTreeState(treeStateProps); // custom hook to handle tree State
return (<Tree data={treeData} />);
}
Why isn't the useEffect hook running when the props change?
Is there a better way to handle this?
Any pointers will be hugely appreciated!
Thanks in advance!

Related

How to effectively initialise client register callback in react functional component or hook

I am trining use factory class in react functional component. I need initialise base on components props and do it in first render or when props change.
AS thats why I created instance and store it ref in useEffect with array that dependent on props to get always new instance when prop changed.
But when I do it a need some special state to force component update.
I am still thinking that there is better and cleaner way how to do it. What is strange is the dummy state to force component to update.
So my code that looks is working looks like this:
export const useClientLoader = (props: IClientLoaderProps) => {
const { backend, workspace, filter } = props;
const [,setInvalidate] = useState(0);
const [initStatus,setInitStatus] = useState<status>("pending");
const loaderRef = useRef<IClientLoader>();
const invalidate = ()=>{
// force component update via changing state
setInvalidate(i=>i+1);
}
useEffect(() => {
// init client instance
loaderRef.current = newClientHandler(backend, workspace, filter);
// update ref not update state and re-render component
// so i do force to update by dummy state update
invalidate();
}, [backend, workspace, filter]); // I need new instance when props changed
const onInitSuccess = () => {
setInitStatus("success");
};
// I need current pointer to client instance
const loader = loaderRef.current;
useEffect(() => {
if (loader) {
// subscribe callback
const onInitSuccessUnsubscribe = loader.onInitSuccess(onInitSuccess);
// init client and do magic on backend and wait for result via callback
loader.init();
return () => {
// Unsubscribe callback
onInitSuccessUnsubscribe();
};
}
}, [loader]); // I will do change just when I have new client instance
return {
initStatus:initStatus,
getUser: loader?.getUser
};
};
I guess you can achieve it with just one useEffect, no need to split on two and then to be forced to use workaround in order to trigger rerender. Like this:
const loaderRef = useRef<IClientLoader>();
const onInitSuccess = useCallback(() => {
setInitStatus("success");
}, []);
useEffect(() => {
loaderRef.current = newClientHandler(backend, workspace, filter);
const onInitSuccessUnsubscribe = loaderRef.current.onInitSuccess(onInitSuccess);
loaderRef.current.init();
return () => {
// Unsubscribe callback
onInitSuccessUnsubscribe();
};
}, [backend, workspace, filter, onInitSuccess]);

Can't update parent state from child using functional components

I am having an issue with my React app. I am trying to set the state of the parent component based on the child component's value. I can see in the dev tools and log window that the child's value is being received by the parent; however, the setState is not working as it should. I have tried creating a separate function just to set the values; hoping for it to act as a middleware but no luck.
I have been through about a couple of StackOverflow threads but not many cater for functional components. I found the following codegrepper snippet for reference but it does not help either.
link: https://www.codegrepper.com/code-examples/javascript/react+function+component+state
Most of the threads deal with how to get the value to the parent component; however, my issue is more "setting the state" specific.
import React, { useEffect, useState } from "react";
import Character from "../component/Character";
import Filter from "../component/Filter";
import Pagination from "../component/Pagination";
import axios from "axios";
import "./Home.css";
const Home = (props) => {
const [API, setAPI] = useState(`https://someapi.com/api/character/?gender=&status=&name=`);
const [characterData, setCharacterData] = useState([]);
const [pagination, setPagination] = useState(0);
const makeNetworkRequest = (data) => {
setAPI(data);
setTimeout(() => {
axios.get(data).then(resp => {
setPagination(resp.data.info)
setCharacterData(resp.data.results)
})
}, 1000)
}
const handleFormCallBack = (childData) => {
setAPI(childData);
makeNetworkRequest(API);
console.log(`Parent handler data ${childData}`)
console.log(`Parent handler API ${API}`)
}
useEffect(() => {
makeNetworkRequest(API)
}, [characterData.length]);
const mappedCharacters = characterData.length > 0 ? characterData.map((character) => <Character key={character.id} id={character.id} alive={character.status} /* status={<DeadOrAlive deadoralive={character.status} /> }*/ gender={character.gender} name={character.name} image={character.image} />) : <h4>Loading...</h4>
return (
<div className="home-container">
<h3>Home</h3>
<Filter parentCallBack={handleFormCallBack} />
<div className="characters-container">
{mappedCharacters}
</div>
{/* <Pagination pages={pagination.pages}/> */}
</div>
)
}
export default Home;
In the code above I am using a callback function on the parent named "handleFormCallBack", mentioned again below to get the information from the child filter component. When I log the value, the following results are being generated.
const handleFormCallBack = (childData) => {
setAPI(childData);
makeNetworkRequest(API);
console.log(`Parent handler data ${childData}`)
// Parent handler data https://someapi.com/api/character/?gender=&status=&name=charactername
console.log(`Parent handler API ${API}`)
// Parent handler API https://someapi.com/api/character/?gender=&status=&name=
}
I am not sure what I am doing wrong but any sort of help would be much appreciated.
Kind Regards
useState works pretty much like setState and it is not synchronous, so when you set the new value using setAPI(childData); react is still changing the state and before it actually does so both of your console.log() statements are being executed.
Solution - after setting the new value you need to track if it has changed, so use a useEffect hook for the endpoint url and then when it changes do what you want.
useEffect(() =< {
// do anything you want to here when the API value changes. you can also add if conditions inside here.
}, [API])
Just to check what I have explained, after calling setAPI(childData); add a setTimeout like
setTimeout(() => {
// you will get new values here. this is just to make my point clear
console.log(Parent handler data ${childData})
console.log(Parent handler API ${API})
}, 5000);

How to subscribe on updates within ReactReduxContext.Consumer?

I would like to figure out how to subscribe on updates of a stored value it the redux store.
So far I've tried something like the following:
<ReactReduxContext.Consumer>
{({store}) => {
console.log('store:', store.getState());
const p = <p>{store.getState().value}</p>;
store.subscribe(() => {p.innerText = store.getState().value});
return p;
}}
</ReactReduxContext.Consumer>
bumping into the TypeError: can't define property "innerText": Object is not extensible error on updates.
So I wonder how to update the contents?
There are a few things about your code that are just not the way that we do things in React.
React is its own system for interacting with the DOM, so you should not attempt direct DOM manipulation through .innerText. Your code doesn't work because the variable p which you create is a React JSX Element rather than a raw HTML paragraph element, so it doesn't have properties like innerText.
Instead, you just return the correct JSX code based on props and state. The code will get updated any time that props or state change.
The ReactReduxContext is used internally by the react-redux package. Unless you have a good reason to use it in your app, I would not recommend it. There are two built-in ways that you can get a current value of state that is already subscribed to changes.
useSelector hook
(recommended)
export const MyComponent1 = () => {
const value = useSelector(state => state.value);
return <p>{value}</p>
}
connect higher-order component
(needed for class components which cannot use hooks)
class ClassComponent extends React.Component {
render() {
return <p>{this.props.value}</p>
}
}
const mapStateToProps = state => ({
value: state.value
});
const MyComponent2 = connect(mapStateToProps)(ClassComponent)
ReactReduxContext
(not recommended)
If anyone reading this has a good reason why they should need to use store.subscribe(), proper usage would look something like this:
const MyComponent3 = () => {
const { store } = useContext(ReactReduxContext);
const [state, setState] = useState(store.getState());
useEffect(() => {
let isMounted = true;
store.subscribe(() => {
if (isMounted) {
setState(store.getState());
}
});
// cleanup function to prevent calls to setState on an unmounted component
return () => {
isMounted = false;
};
}, [store]);
return <p>{state.value}</p>;
};
CodeSandbox Demo

React updating UI after a change in Props feeded to useReducer

I have a React Functional Component that behaves in this way:
const Child = ({objValues, number}) => {
const emptyState = {
a: { b: "", c:""}
d: { e: "", f:""}
}
const initialState = {...emptyState, ...objValues}
const [state, dispatch] = useReducer(reducer, {...initialState})
return (
// render something based on state's nested objects.
// and the number prop.
)
}
const Parent = () => {
[objValues, setObjValues] = useState({});
[number, setNumber] = useState("");
//... some network requests to populate values
// and change number (using setState);
useEffect(() => {
// fetchAPI and then set values using spread operator
// to force creating a new object.
setObjValues({...values})
}, [number])
return ( <Child objValues={objValues} number={number}/>
I always get unique items (number, objValues) from the API I am using. Inside the component, I see that whenever I fire a new network request, the value of number gets updated in the UI, but the objValues shows the same values as the previous object. In the Component tabs in the react dev tools, I see that these values do update, nevertheless the UI stays the same.
const Child = ({objValues, number}) => {
const emptyState = {
a: { b: "", c:""}
d: { e: "", f:""}
}
const initialState = {...emptyState, ...objValues}
const [state, dispatch] = useReducer(reducer, {...initialState})
useEffect(()=>{
dispatch({
...objValues
})
},[objValues])
return (
// render something based on state's nested objects.
// and the number prop.
)
}
Couple of things
do not use the spread everywhere, dont see a reason for any of them in your code, if you pass object to setState it will take the current value and put it to state as value not reference so no reason to "force"
try typescript or try to not mix types, in your numer state you have default value a string, either have it empty, null or something that is close to a number
To your problem: since the reducer will resolve data, I dont think it has to update based on change in its initial data, what you should do, is ditch this populating some data and mixing them with redux data, but make the api request and once its done, save the data to redux and your reducer in another component will update itself
EDIT: Iam dumb its not useSelector from redux, regardless initial data got to be used only once, and they dont force the hook to update its value, basicaly any solid library for react or react itself, will look at initial data only when its called the first time, and then it doesnt matter how many times or how hard you change the initial data, it shouldnt affect it (most component or form libraries do follow this rule), basically in this case I would do useeffect and I would update the reducer with the data, or if that is not possible declare new constant, where you spread your default value and then spread your reducer data

When using useState hook - Is there an importance to changing the setState functions call order?

I have a React functional component with two state variables (itemsData & itemsCollections). The variables are updated in the useEffect method. But after useEffect occur one of the state variables is null.
Upon switching the setStateFunctions (setItemsData & setItemsCollect) call order both arguments are inialized as expected.
How's that?
const MyComponent = ({itemsIds}) => {
const [itemsData, setItemsData] = useState([]);
const [itemsCollections, setItemsCollect] = useState({});
useEffect(() => {
fetchItemsData({ itemsIds }).then(({ items, itemCollect }) => {
setItemsData(items);
setItemsCollect(itemCollect);
})
}, [itemsIds]);
...
console.log('itemsData', itemsData) // the expected array
console.log('itemCollect', itemCollect) // empty objecy
State after useEffect: itemCollect = {}, itemsData = [{value:...},...]
Switching the order of the calls:
const MyComponent = ({itemsIds}) => {
...
useEffect(() => {
fetchItemsData({ itemsIds }).then(({ items, itemCollect }) => {
setItemsCollect(itemCollect); // <--> switched rows
setItemsData(items); // <--> switched rows
})
}, [itemsIds]);
...
console.log('itemsData', itemsData) // the expected array
console.log('itemCollect', itemCollect) // the expected object
State after useEffect: itemCollect = { someValue: ...} , itemsData = [{value:...},...]
There is a performance optimization called batching, which can change between React versions. When this optimization is applied, multiple setState calls will be batched together before the next render (and the order does not matter).
When not applied (e.g. inside a Promise as in your case, see Does React batch state update functions when using hooks?), then each state update will trigger a new render (and the order matters).
=> console.log('itemCollect', itemCollect) may log different data in each render.
If you need to force a single state update, then calling a single dispatch from useReducer might be the best option.

Categories

Resources