Why isn't my React Hook updating when I setState? - javascript

I'm using react useState, where the state is an object with some nested properties. When I call setState on it, I'm not seeing a re-render or the state being updated. I assume react is seeing that the new state equals the old state and so no updates occur. So, I've tried cloning the state first, but still am not seeing any updates.
How can I get this function to cause the state to update?
export type TermEditorStateRecord = {
term: SolrTermType;
state: SolrTermEditorRecordState;
classifications: { [key: string]: string };
};
export type TermEditorStateRecordMap = {
[phrase: string]: TermEditorStateRecord;
};
const [records, setRecords] = useState({});
const setRecordClassification = (label, key, value) => {
const cloned = new Object(records) as TermEditorStateRecordMap;
cloned[label].classifications[key] = value;
setRecords(cloned);
};
I apologize for the TypeScript types, but I've included them here so that you can see the expected shape of the state.
Is it not updating because the changes are deeply nested? Is there a way to get this to work, or do I need to somehow pull the state that changes out into its own state?

new Object does not make a deep copy, so for setRecords it's the same instance and it won't trigger the re-render,
const obj = {
a: {
b: "b"
}
};
const copy = new Object(obj);
copy["c"] = "c";
console.log(obj);
You'll need to manually updated the nested property :
const setRecordClassification = (label, key, value) => {
setRecords(record => ({
...record,
[label]: {
...record[label],
classifications: {
...record[label].classifications,
[key]: value
}
}
}));
};
or to create a copy, use :
const cloned = JSON.parse(JSON.stringify(record));
cloned[label].classifications[key] = value;
setRecords(cloned);

Related

How to make React functional component recreate callback function and read updated props

I have a very simple functional component in React. When this component is rendered by the parent component, initially myList is an empty array, and then eventually when it finishes loading, it is a list with a bunch of items.
The problem is, the value of myList inside onSearchHandler never gets updated, it's always [].
const MyComponent = ({ myList }) => {
const [filteredList, setFilteredList] = useState(myList);
console.log(myList); // <<< This outputs [], and later [{}, {}, {}] which is expected.
useEffect(() => {
setFilteredList(myList);
}, [myList]);
const onSearchHandler = (searchText) => {
console.log(myList); /// <<< When this function is called, this always outputs []
const filteredItems = myList.filter(item =>
item.name.toLowerCase().includes(searchText.toLowerCase())
);
setFilteredList(filteredItems);
};
return <AnotherComponent items={filteredList} onSearch={onSearchHandler} />
};
Is there a way to force onSearchHandler to re-evaluate the value of myList? What would be the recommended approach for this sort of operation?
It sounds like AnotherComponent does not take into consideration the changed prop - this should be considered to be a bug in AnotherComponent. Ideally, you'd fix it so that the changed prop gets used properly. Eg, just for an example, maybe it's doing
const [searchHandler, setSearchHandler] = useState(props.onSearch);
and failing to observe prop changes as it should. Or, for another random example, this could happen if the listener prop gets passed to an addEventListener when the component mounts but again doesn't get checked for changes and removed/reattached.
If you can't fix AnotherComponent, you can use a ref for myList in addition to the prop:
const MyComponent = ({ myList }) => {
const myListRef = useRef(myList);
useEffect(() => {
myListRef.current = myList;
setFilteredList(myList);
}, [myList]);
const [filteredList, setFilteredList] = useState(myList);
const onSearchHandler = (searchText) => {
const filteredItems = myListRef.current.filter(item =>
item.name.toLowerCase().includes(searchText.toLowerCase())
);
setFilteredList(filteredItems);
};
It's ugly, but it might be your only option here.

Cannot access react state from callback

I have the following components:
const ParentComponent: React.FC = () => {
// Somewhere in the code, I set this to some value
const [newType, setNewType] = useState<any>(undefined);
// Somewhere in the code, I set this to true
const [enableAddEdge, setEnableAddEdge] = useState(false);
const addEdge = (data: any) => {
console.log("newType", newType); // This is undefined
}
return (
...
<VisNetwork
newType={newType}
onAddEdge={addEdge}
enableAddEdge={enableAddEdge}
/>
...
)
}
export default ParentComponent;
interface Props {
newType: any;
onAddEdge: (data: any) => void;
enableAddEdge: boolean;
}
const VisNetwork: React.FC<Props> = (props: Props) => {
const options: any = {
// Some vis-network specific option configuration
manipulation: {
addEdge: (data: any, callback: any) => props.onAddEdge(data);
}
}
...
// Some code to create the network and pass in the options
const network = new vis.Network(container, networkData, options);
useEffect(() => {
if (props.enableAddEdge) {
// This confirms that indeed newType has value
console.log("newType from addEdge", props.newType);
// Call reference to network (I name it currentNetwork)
// This will enable the adding of edge in the network.
// When the user is done adding the edge,
// the `addEdge` method in the `options.manipulation` will be called.
currentNetwork.addEdgeMode();
}
}, [props.enableAddEdge])
useEffect(() => {
if (props.newType) {
// This is just to confirm that indeed I am setting the newType value
console.log("newType from visNetwork", props.newType); // This has value
}
}, [props.newType]);
}
export default VisNetwork;
When the addEdge method is called, the newType state becomes undefined. I know about the bind but I don't know if it's possible to use it and how to use it in a functional component. Please advise on how to obtain the newType value.
Also, from VisNetwork, I want to access networkData state from inside options.manipulation.addEdge. I know it's not possible but any workaround? I also need to access the networkData at this point.
You need to useRef in this scenario. It appears const network = new vis.Network(container, networkData, options); uses the options from the first render only. Or something similar is going on.
It's likely to do with there being a closure around newType in the addEdge function. So it has stale values: https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function
In order to combat this, you need to useRef to store the latest value of newType. The reference is mutable, so it can always store the current value of newType without re-rendering.
// Somewhere in the code, I set this to some value
const [newType, setNewType] = useState<any>(undefined);
const newTypeRef = useRef(newType);
useEffect(() => {
// Ensure the ref is always at the latest value
newTypeRef.current = newType;
}, [newType])
const addEdge = (data: any) => {
console.log("newType", newTypeRef.current); // This is undefined
}

useSelector not updating when store has changed in Reducer. ReactJS Redux

I am changing the state in reducer. On debug I checked that the state was really changed. But the component is not updating.
Component:
function Cliente(props) {
const dispatch = useDispatch()
const cliente = useSelector(({ erpCliente }) => erpCliente.cliente)
const { form, handleChange, setForm } = useForm(null)
...
function searchCepChangeFields() {
//This call the action and change the store on reducer
dispatch(Actions.searchCep(form.Cep))
.then(() => {
// This function is only taking values ​​from the old state.
// The useSelector hook is not updating with store
setForm(form => _.setIn({...form}, 'Endereco', cliente.data.Endereco))
setForm(form => _.setIn({...form}, 'Uf', cliente.data.Uf))
setForm(form => _.setIn({...form}, 'Cidade', cliente.data.Cidade))
setForm(form => _.setIn({...form}, 'Bairro', cliente.data.Bairro))
})
}
Reducer:
case Actions.SEARCH_CEP:
{
return {
...state,
data: {
...state.data,
Endereco: action.payload.logradouro,
Bairro: action.payload.bairro,
UF: action.payload.uf,
Cidade: action.payload.cidade
}
};
}
NOTE: you better start using redux-toolkit to prevent references
in you code its a better and almost a must way for using redux
the problem your facing is very common when handling with objects,
the props do not change because you're changing an object property but the object itself does not change from the react side.
even when you're giving it a whole new object
react doesn't see the property object change because the reference stays the same.
you need to create a new reference like this:
Object.assign(state.data,data);
return {
...state,
data: {
...state.data,
Endereco: action.payload.logradouro,
Bairro: action.payload.bairro,
UF: action.payload.uf,
Cidade: action.payload.cidade
}
}
to add more you can learn about the Immer library that solves this
problem.
It's not necessary to
Object.assign(state.data, data);
always when changing data of arrays or objects
return(
object: {...state.object, a: 1, b: 2},
array: [...state.array, 1, 2, 3]
)
this 3 dots (...) ensure that you create a new object. On redux you have to always create a new object, not just update the state. This way redux won't verify that your data has changed.
When having nesting objects or arrays, is the same thing
Just have attention to:
initialState = {
object: {
...object,
anotherObject:{
...anotherObject,
a: 1,
b: 2
}
}
}
Somehow, the Object.assgin is not recognize
Update with ES6 syntax.
updatedConnectors = state.connectors
This will create a reference to the current state. In ES6, that introduce the ... to make new reference.
updatedConnectors = { ...state.connectors }
.....
return {
...state,
connectors: updatedConnectors
};
use this to extract and copy new reference. That will trigger state change too
Update Sep/27/20
I've wrote some utils function to handle this, Let try this
//Utils
export const getStateSection = ({ state, sectionName }) => {
const updatedState = { ...state }
const updatedSection = updatedState[sectionName]
return updatedSection
}
export const mergeUpdatedSection = ({ state, sectionName, updatedValue }) => {
const updatedState = { ...state }
updatedState[sectionName] = updatedValue
return updatedState
}
Then In any reducer, It should use like this:
//reducer
const initState = {
scheduleDetail: null,
timeSlots: null,
planDetail: {
timeSlots: [],
modifedTimeSlots: [],
id: 0
}
}
.....
const handlePickTimeSlot = (state, action) => {
let planDetail = getStateSection({ state, sectionName: 'planDetail' })
// do stuff update section
return mergeUpdatedSection({ state, sectionName: 'planDetail', updatedValue: planDetail })
}
Since the edit queue for elab BA is full.
The accepted answer here is what he meant by data being there
case MYCASE:
let newDataObject = Object.assign(state.data, {...action.payload});
// or
// let newDataObject = Object.assign(state.data, {key: 'value', 'key2': 'value2' ...otherPropertiesObject);
return {
...state,
...newDataObject
}
There is an interesting edge case that can happen when modifying the file where you create your Store.
If the file where you have your redux store Provider component (usually App.tsx) does not get reloaded by React's hot module reloader (HMR) but the redux store file gets modified and therefore reloaded by HMR, a new store is created and the store Provider in your App.tsx can actually end up passing an old instance of your redux store to useSelector.
I have left the following comment in my setup-store.ts file:
/**
* Note! When making changes to this file in development, perform a hard refresh. This is because
* when changes to this file are made, the react hot module reloading will re-run this file, as
* expected. But, this causes the store object to be re-initialized. HMR only reloads files that
* have changed, so the Store Provider in App.tsx *will not* be reloaded. That means useSelector
* values will be querying THE WRONG STORE.
*/
It is not a problem, you should understand how the React is working. It is expected behavior
In your case you are invoking
dispatch(Actions.searchCep(form.Cep))
.then(() => {...some logic...}
But all of this work in ONE render, and changed store you will get only in the NEXT render. And in then you are getting props from the first render, not from the next with updated values. Look for example below:
import React from 'react';
import { useSelector, useDispatch } from "react-redux";
import { selectCount, incrementAsync } from "./redux";
import "./styles.css";
export default function App() {
const value = useSelector(selectCount);
const dispatch = useDispatch();
const incrementThen = () => {
console.log("value before dispatch", value);
dispatch(incrementAsync(1)).then(() =>
console.log("value inside then", value)
);
};
console.log("render: value", value);
return (
<div className="App">
<p>{value}</p>
<button onClick={incrementThen}>incrementThen</button>
</div>
);
}
And output
value before dispatch 9
render: value 10
value inside then 9

Is there such a thing as a "correct" way of defining state with React hooks and Typescript?

I've been working with React for a while, and yesterday I got my feet wet with hooks in a Typescript based project. Before refactoring, the class had a state like this:
interface INavItemProps {
route: IRoute;
}
interface INavItemState {
toggleStateOpen: boolean
}
class NavItem extends Component<INavItemProps, INavItemState> {
constructor() {
this.state = { toggleStateOpen: false };
}
public handleClick = (element: React.MouseEvent<HTMLElement>) => {
const toggleState = !this.state.toggleStateOpen;
this.setState({ toggleStateOpen: toggleState });
};
...
}
Now, when refactoring to a functional component, I started out with this
interface INavItemProps {
route: IRoute;
}
const NavItem: React.FunctionComponent<INavItemProps> = props => {
const [toggleState, setToggleState] = useState<boolean>(false);
const { route } = props;
const handleClick = (element: React.MouseEvent<HTMLElement>) => {
const newState = !toggleState;
setToggleState(newState);
};
...
}
But then I also tested this:
interface INavItemProps {
route: IRoute;
}
interface INavItemState {
toggleStateOpen: boolean
}
const NavItem: React.FunctionComponent<INavItemProps> = props => {
const [state, setToggleState] = useState<INavItemState>({toggleStateOpen: false});
const { route } = props;
const handleClick = (element: React.MouseEvent<HTMLElement>) => {
const newState = !state.toggleStateOpen;
setToggleState({toggleStateOpen: newState});
};
...
}
Is there such a thing as a correct way of defining the state in cases like this? Or should I simply just call more hooks for each slice of the state?
useState hook allows for you to define any type of state like an Object, Array, Number, String, Boolean etc. All you need to know is that hooks updater doesn't merge the state on its own unline setState, so if you are maintain an array or an object and you pass in only the value to be updated to the updater, it would essentially result in your other states getting lost.
More often than not it might be best to use multiple hooks instead of using an object with one useState hook or if you want you can write your own custom hook that merges the values like
const useMergerHook = (init) => {
const [state, setState] = useState(init);
const updater = (newState) => {
if (Array.isArray(init)) {
setState(prv => ([
...prv,
...newState
]))
} else if(typeof init === 'object' && init !== null) {
setState(prv => ({
...prv,
...newState
}))
} else {
setState(newState);
}
}
return [state, updater];
}
Or if the state/state updates need to be more complex and the handler need to be passed down to component, I would recommend using useReducer hook since you have have multiple logic to update state and can make use of complex states like nested objects and write logic for the update selectively

Redux form initial values with Map()

i have a redux form and i want to set the initial values property to a Map and not an object. This is mandatory and i cannot change this :P
Therefore i set an initialValues Map like this:
export const initialValuesForm = (timeSlots, formValues) => {
const initialValues = Map();
const date = formValues[DATE_PICKER_FORM_FIELD] && formValues[DATE_PICKER_FORM_FIELD].startDate;
const travelers = formValues[TRAVELERS_FORM];
initialValues[POPUP_DATE_PICKER] = date;
initialValues[TRAVELERS_FORM_SELECT] = travelers;
return initialValues;
};
The formValues is an object with the values of another form from the state.
The very weird situation is that when i open the pop up with the form the values object and initial object are an empty object. When i change a value all the values are updated and are inside the Map. My mapStatetoProps function is this:
const mapStateToProps = (state) => {
return {
availability,
locale,
activityToBeAdded,
popUpFormErrors,
initialValues: initialValuesForm(timeSlots, searchFormValues)
};
};
Why is this happening? Any ideas?

Categories