Related
In my React component, I have the following code:
const [additionalGuestAmounts] = useState<RatePlan['additionalGuestAmounts']>(
useAdditionalGuestAmounts(roomType, occupantsForBaseRate)
)
And here is my custom hook:
const useAdditionalGuestAmounts = (
roomType: RoomType,
occupantsForBaseRate: number
): RatePlan['additionalGuestAmounts'] => {
console.log('executing useAdditionalGuestAmounts hook')
if (!roomType) {
return []
}
console.log('returning array')
return [
{
foo: 'bar'
},
]
}
When I modify roomType from null to a value, it logs 'executing useAdditionalGuestAmounts hook' and then 'returning array', however the value of additionalGuestAmounts is not updated; it stays as an empty array. Why is the value not being updated if the custom hook is being executed?
The value that is passed in the useState hook is only the initial value of the state as explained on the documentation of the useState hook: https://reactjs.org/docs/hooks-state.html, to make sure that the state is updated whenever the useAdditionalGuestAmounts hook is called we will have to call setAdditionalGuestAmounts with the new value. This would then look like:
// Initial state
const [additionalGuestAmounts, setAdditionalGuestAmounts] = useState<RatePlan['additionalGuestAmounts']>([])
const useAdditionalGuestAmounts = (
roomType: RoomType,
occupantsForBaseRate: number
) => {
if (!roomType) {
return []
}
// Update the state, this causes the page to re-render
setAdditionalGuestAmounts( [
{
foo: 'bar'
},
])
}
If you need the useAdditionalGuestAmounts to be called on page load you can use the useEffect hook for this (https://reactjs.org/docs/hooks-effect.html)
The value of the additionalGuestAmounts state variable is not being updated when you modify roomTypeId because the useAdditionalGuestAmounts hook is not being called again. This is because the useAdditionalGuestAmounts hook is only called when the component is first rendered, and is not called again.
As stated previously by rowan, you can a useEffect hook inside the useAdditionalGuestAmounts hook.
const useAdditionalGuestAmounts = (
roomType: RoomType,
occupantsForBaseRate: number
): RatePlan['additionalGuestAmounts'] => {
console.log('executing useAdditionalGuestAmounts hook')
const [additionalGuestAmounts, setAdditionalGuestAmounts] = useState<
RatePlan['additionalGuestAmounts']
>([]);
useEffect(() => {
if (!roomType) {
return;
}
console.log('returning array');
setAdditionalGuestAmounts([
{
foo: 'bar',
},
{
foo: 'bar',
},
{
foo: 'bar',
},
{
foo: 'bar',
},
{
foo: 'bar',
},
]);
}, [roomTypeId, occupantsForBaseRate]);
return additionalGuestAmounts;
};
You also need to add the occupantsForBaseRate argument to the dependencies array, because the useAdditionalGuestAmounts hook depends on it.
Hope this helps.
I'm trying to figure out why my useEffect hook keeps getting called multiple times, even when the dependency has the same value. I'm using the following code:
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import Cards from '../../../cards/Cards'
import UserCard from '../../../cards/users/Card'
import LoadingContainer from '../../../LoadingContainer'
import UsersResource from '../../../../api/resources/Users'
const Users = ({ users }) => (
<Cards>
{users.map((user) => (
<UserCard user={user} key={`user-${user.id}`} />
))}
</Cards>
)
const UsersPage = () => {
const [initialLoad, setInitialLoad] = useState(true)
const [loading, setLoading] = useState(true)
const [initialUsers, setInitialUsers] = useState([])
const [users, setUsers] = useState([])
const fetchUsers = async () => {
setLoading(true)
const response = await UsersResource.getIndex()
setInitialUsers(response.data)
}
useEffect(() => {
fetchUsers()
}, [])
useEffect(() => {
console.log('users changed:', users)
initialLoad ? setInitialLoad(false) : setLoading(false)
}, [users])
useEffect(() => {
setUsers(initialUsers)
}, [initialUsers])
return (
<LoadingContainer
loading={loading}
hasContent={!!users.length}
>
<Users users={users} />
</LoadingContainer>
)
}
Users.propTypes = {
users: PropTypes.arrayOf(PropTypes.shape).isRequired,
}
export default UsersPage
This is the effect that gets re-run when the value of the users dependency stays the same:
useEffect(() => {
console.log('users changed:', users)
initialLoad ? setInitialLoad(false) : setLoading(false)
}, [users])
Here's the output:
users changed: []
users changed: []
users changed: (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
So users is obviously being recognized as changed twice, even though both times the effect is called, it returns the same value. This results in my loading state being set to false before the request finishes.
It only runs once if I change the initial state assignment of users from this...
const [users, setUsers] = useState([])
To this...
const [users, setUsers] = useState(initialUsers)
This tells me that the component must be rerendering simply because users is pointing to initialUsers in the second effect, instead of just a blank array (even though initialUsers returns a blank array as well). Can anyone explain why this happens this way? I can't seem to find any documentation describing this behavior (maybe I'm blind).
I would expect the value to be the only thing to influence an effect, but it seems like it might get triggered because the dependency is pointing to a new reference in memory. Am I off?
This appears to be a bit of a misunderstanding between value equality and reference equality. React uses reference equality.
The initial initialUsers and users state values are [], and on the initial render cycle there is a useEffect hook that enqueues an update to users with the current initialUsers value.
Note that initialUsers isn't not the same reference as users, so initialUsers === users evaluates false.
const initialUsers = [];
const users = [];
console.log(initialUsers === users); // false
Note also that [] === [] is also never true since they are two object references.
console.log([] === []); // false
This is roughly how the logic flows:
On the initial render cycle the initial users state [] is logged in the second useEffect hook.
The useEffect with dependency on initialUsers runs and updates the users state to the value of the initialUsers state. [] (but a different reference).
The second useEffect hook logs the users state update, again [].
The fetchUsers handler has fetched data and enqueues an update to the initialUsers state.
The second useEffect hook logs the users state update, now a populated array.
Code:
const fetchUsers = async () => {
setLoading(true);
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
// (4) update initialUsers
setInitialUsers(response.data);
};
useEffect(() => {
fetchUsers();
}, []);
useEffect(() => {
// (1) initial render, first log "[]"
// (3) second render, second log "[]"
// (5) third render, third log "[.........]"
console.log("users changed:", users);
initialLoad ? setInitialLoad(false) : setLoading(false);
}, [users]);
useEffect(() => {
// (2) initial render update users
setUsers(initialUsers);
}, [initialUsers]);
The difference when you initialize the users state to the initialState value is now they are the same reference.
const initialUsers = [];
const users = initialUsers;
console.log(initialUsers === users); // true
This subtle difference skips the enqueued update #2 above since users and initialUsers are already the same reference.
I have some state variables which will change at some point in time. I want to know what is the proper way of handling such a huge amount of input variables. I had used the same code in Vue.js and the two-way data-binding works nicely for me. What will be the appropriate way to handle the below variables?
data() {
return {
valid: false,
showDialog: true,
showFullLoading: false,
isImage: false,
product: {
name_en: "",
name_ar: "",
category: "",
subcategory: "",
description_en: "",
description_ar: "",
meta_title: "",
meta_description: "",
meta_keywords: "",
price: 0.0,
showSale: false,
sale: 0.0,
image: "",
saleAfterStock: false,
stock: 10
},
images: [
{ image: null, isImage: false, id: shortid.generate() },
{ image: null, isImage: false, id: shortid.generate() },
{ image: null, isImage: false, id: shortid.generate() },
{ image: null, isImage: false, id: shortid.generate() }
],
attributes: [],
defaultVariants: [],
dataWeightClass: ["Gram", "Kilo Gram", "Pound"],
dataDimensionClass: ["Centimeter", "Inch"],
showAttributeDialog: false,
sizeGuide: false,
sizeGuides: [],
attribute: {
title_en: "",
title_ar: "",
description_en: "",
description_ar: "",
image: null,
isImage: false
},
subcategories: [],
options: [],
variantsHeaders: [
{ text: "Variant", value: "name" },
{ text: "Price", value: "price" },
{ text: "Stock", value: "quantity" },
{ text: "Visibility", value: "visibility" }
],
defaultVariantId: "",
defaultPreviewId: ""
};
},
This is the data object in vue. I want to convert these data objects to state in react.
Thanks.
You've said you're going to write a function component. In function components, the usual ways (in React itself) to store state are useState and useReducer:
useState -
Returns a stateful value, and a function to update it.
useReducer -
Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. (If you’re familiar with Redux, you already know how this works.)
Outside React, there are tools like Redux (with React bindings), Mobx, and others that provide advanced state management.
Sticking with what's in React, if the various parts of your state object change in relation to one another, you probably want useReducer. If they're fairly distinct, you probably want useState (though some people never use useReducer, and others never use useState). From the useReducer documentation:
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
useState
Using useState, you can have a megalithic state object like your data object, or you can break it up into individual parts. I would default to breaking it up into parts and only use a megalithic object if you have to, not least because updates are simpler. (Although that said, your data object is relatively flat, which means it's not that hard to update.)
Here's what that might look like:
function MyComponent(props) {
const [valid, setValid] = useState(false);
const [showDialog, setShowDialog] = useState(true);
// ...
const [product, setProduct] = useState({
name_en: "",
name_ar: "",
category: "",
subcategory: "",
description_en: "",
description_ar: "",
meta_title: "",
meta_description: "",
meta_keywords: "",
price: 0.0,
showSale: false,
sale: 0.0,
image: "",
saleAfterStock: false,
stock: 10,
});
// ...
const [images, setImages] = useState([
{ image: null, isImage: false, id: shortid.generate() },
{ image: null, isImage: false, id: shortid.generate() },
{ image: null, isImage: false, id: shortid.generate() },
{ image: null, isImage: false, id: shortid.generate() },
]);
// ...
}
You might define those initial values outside the component so you don't recreate objects and arrays unnecessarily, and for code clarity. For instance, if you had your existing data object as defaults, the above might be:
const defaults = { /*...*/ };
function MyComponent(props) {
const [valid, setValid] = useState(defaults.valid);
const [showDialog, setShowDialog] = useState(defaults.showDialog);
// ...
const [product, setProduct] = useState(defaults.product);
// ...
const [images, setImages] = useState(defaults.images);
// ...
}
Either way, you then update those state members with calls to their setters. For valid and other simple primitives, it's very straight-forward, either:
setValid(newValue);
...or, if you're using the old value when setting the new one (like toggling), use the callback form so you're sure to get up-to-date state information:
setValid((oldValue) => !oldValue);
For objects and arrays, it's more complicated. You have to create a new object or array with the changes you want. For instance, to update product's category:
setProduct((oldProduct) => { ...oldProduct, category: "new category" });
Notice how we make a copy of the old product object, then apply our update to it. The copy can be shallow (which is what spread syntax does).
To update images using an id variable that says which image to update, and (let's say) you want to set isImage to true:
setImages((oldImages) => oldImages.map((image) => {
if (image.id === id) {
return { ...image, isImage: true }; // Or whatever change it is
}
return image;
}));
Again, notice we didn't just assign a new image object to the existing array, we created a new array (in this case via map).
If you used a megalithic state object, like this:
function MyComponent(props) {
const [state, setState] = useState(defaults);
// ...
}
...then updating (say) images becomes a bit more difficult, although again your state object is fairly shallow so it's not that bad:
setState((oldState) => ({
...oldState,
images: oldState.images.map((image) => {
if (image.id === id) {
return { ...image, isImage: true }; // Or whatever change it is
}
return image;
})
}));
The key thing either way (individual or megalithic) is that any object or array that's a container for what you're changing (directly or indirectly) has to be copied. It's just that with a megalithic structure, you end up doing more copies since everything is inside a single container.
useReducer
With useReducer, you again have the option of a single megalithic state object that the dispatch function updates (via your reducer function), or individual ones, each with their own dispatch and (probably) reducer functions. With useReducer, it's more common to have large state objects because one of the primary use cases is large structures where different parts may be updated by a single action. That might mean megalithic, or just larger chunks, etc.
The main difference is that your code in the component sends "actions" via dispatch, and then it's code in the dispatch function you write that does the updates to the state object and returns the new one.
Suppose you used useReducer with your megalithic object:
function MyComponent(props) {
const [state, dispatch] = useReducer(reducer, defaults);
// ...
}
Your reducer might look like this:
function reducer(state, action) {
const { type, ...actionParams } = action;
switch (type) {
case "update-image":
const { id, ...updates } = actionParams;
return {
...state,
images: state.images.map((image) => {
if (image.id === id) {
return { ...image, ...updates };
}
return image;
})
};
// ...other actions...
}
}
and then in your component, instead of the setImages call shown in the useState section, you might do:
dispatch({ type: "update-image", id, isImage: true });
or similar (there are lots of different ways to write reducer functions).
The same rule applies to the state updates your reducer returns as to useState setters: Any object or array containing what you're updating has to be copied. So in this example copy the overall state object, the images array, and the image object we're updating. As with useState, if you used more individual useReducers, you'd do a bit less copying.
If you are using react's functional approach you can use useReducer hooker in order to locally (in the componente level) manage with state, or use redux to globally (app level) do the same.
You create an myreducer.js file and put something like this
export const myReducer = (state, action) => {
switch (action.type) {
case "UPDATE_PRICE": {
return {
...state,
product: {
...state.product,
price: action.product.price
}
};
}
};
export const myReducer = (state, action) => {
switch (action.type) {
case "UPDATE_PRICE": {
state.product.price = action.product.price;
return state;
}
}
};
in your component you can use useReducer hook (if you just want to manage state locally)
import { useReducer } from 'react'
import { myReducer } from 'myReducer'
export const MyComponent = () => {
const [myState, dispatchMyState] = useReducer(myReducer, {})
...
//where you need to update you can use this
const handleAction = () => {
dispatchMyState({
type: "UPDATE_PRICE",
dataToUpdate
});
}
return <div onClick={handleAction}> anything</div>
}
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
Component to test
class Carousel extends React.Component {
state = {
slides: null
}
componentDidMount = () => {
axios.get("https://s3.amazonaws.com/rainfo/slider/data.json").then(res => {
this.setState({ slides: res.data })
})
}
render() {
if (!slides) {
return null
}
return (
<div className="slick-carousel">
... markup trancated for bravity
</div>
)
}
}
export default Carousel
Test
import React from "react"
import renderer from "react-test-renderer"
import axios from "axios"
import Carousel from "./Carousel"
const slides = [
{
ID: "114",
REFERENCE_DATE: "2018-07-02",
...
},
{
ID: "112",
REFERENCE_DATE: "2018-07-06",
...
},
...
]
jest.mock("axios")
it("", () => {
axios.get.mockImplementationOnce(() => Promise.resolve({ data: slides }))
const tree = renderer.create(<Carousel />).toJSON()
expect(tree).toMatchSnapshot()
})
snapshot only records null, since at the moment of execution I suppose state.slides = null.
Can't put my finger on how to run expectations after axios done fetching the data.
Most of the samples online either use enzyme, or show tests with async functions that return promises. I couldn't find one that would show example only using jest and rendered component.
I tried making test function async, also using done callback, but no luck.
in short:
it("", async () => {
axios.get.mockImplementationOnce(() => Promise.resolve({ data: slides }))
const tree = renderer.create(<Carousel />);
await Promise.resolve();
expect(tree.toJSON()).toMatchSnapshot()
})
should do the job
in details: besides you have mocked call to API data is still coming in async way. So we need toMatchSnapshot call goes to end of microtasks' queue. setTimeout(..., 0) or setImmediate will work too but I've found await Promise.resolve() being better recognizable as "everything below is coming to end of queue"
[UPD] fixed snippet: .toJSON must be after awaiting, object it returns will never be updated
The accepted answer started to fail the next day. After some tweaking, this seems to be working:
import React from "react"
import renderer from "react-test-renderer"
import axios from "axios"
import Carousel from "./Carousel"
jest.mock("axios")
const slides = sampleApiResponse()
const mockedAxiosGet = new Promise(() => ({ data: slides }))
axios.get.mockImplementation(() => mockedAxiosGet)
// eventhough axios.get was mocked, data still comes anychrnonously,
// so during first pass state.slides will remain null
it("returns null initally", () => {
const tree = renderer.create(<Carousel />).toJSON()
expect(tree).toMatchSnapshot()
})
it("uses fetched data to render carousel", () => {
const tree = renderer.create(<Carousel />)
mockedAxiosGet.then(() => {
expect(tree.toJSON()).toMatchSnapshot()
})
})
function sampleApiResponse() {
return [
{
ID: "114",
REFERENCE_DATE: "2018-07-02",
...
},
{
ID: "114",
REFERENCE_DATE: "2018-07-02",
...
},
]
}