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.
CodePudding user response:
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 adispatch
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 touseState
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 passdispatch
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 useReducer
s, you'd do a bit less copying.