When should I put state in parent? - javascript

This is my first StackOverflow question, feel free to add any suggestion you want.
I have an AudioControls component that renders four different Controls components, each for a different audio option (volume, bass, mid, treble).
I wrote it in a way that the parent knows and manage the four of the states:
useState({
volume: 50,
bass: 50,
mid: 50,
treble: 50,
})
and then passed these values to children as props.
I have two concerns regarding this:
Control components are simply the same and maybe they could have their own value state (put an audioValue state on each Control). I've read it's a good practice to always lift the state when possible, but why is it needed? What are good reasons to do it?
Is there a way to use the map function with these Controls given that they are all pretty similar? Without losing legibility, of course.
This is the AudioControl code:
function AudioControls() {
const [audioValues, setAudioValues] = useState({
volume: 50,
bass: 50,
mid: 50,
treble: 50
})
function handleControl(option, id) {
const oldValue = audioValues[id]
const newValue = option === "+" ? oldValue + 1 : oldValue - 1
setAudioValues(prevState => (
{
...prevState,
[`${id}`] : newValue
}
))
}
return(
<>
<Control
id="volume"
name="Volume"
value={audioValues.volume}
handleControl={handleControl}
/>
<Control
id="bass"
name="Bass"
value={audioValues.bass}
handleControl={handleControl}
/>
<Control
id="mid"
name="Mid"
value={audioValues.mid}
handleControl={handleControl}
/>
<Control
id="treble"
name="Treble"
value={audioValues.treble}
handleControl={handleControl}
/>
</>
)
}
And here's the Control component:
function Control({ id, name, value, handleControl }) {
return(
<div>
<button onClick={() => handleControl("+", id)}>
+
</button>
<label>{name}: {value}</label>
<button onClick={() => handleControl("-", id)}>
-
</button>
</div>
)
}

This is my second stack overflow answer. Also open to any and all feedback.
I'd be curious to learn where you heard the fact that it's always a good idea to lift state. My thought on that is: if it's always a good idea to lift state, when do you stop lifting? Why just move state up one in this case? Why not lift it even further?
To answer your first question: the main reason to lift state into a parent component is if you need two sibling components to use the same piece of state. For example:
const [volume, setVolume] = setState(50);
function changeVolume (newVolume) {
setVolume(newVolume);
}
<VolumeDisplay volume={volume} />
<VolumeControl onChange={changeVolume} />
In your case, it looks like each one is capable of changing its own state so it's preferred to push state down to each <Control>. Once you put the audio value locally inside the control component you could map them easily like this:
const controls = [
{id: "bass", name: "Bass"},
{id: "volume", name: "Volume"},
{id: "mid", name: "Mid"},
{id: "treble", name: "Treble"},
]
return (
<>
{controls.map(({id, name}) => <Control id={id} name={name} />)}
<>
)

Related

React rerender component using hooks when property in an array of object changes

So I am in a situation where I have to change a particular property from an array of objects. When the property changes I want to rerender the component. Now, this works fine without any issues when use the setPropertyName of useState. But now I am just changing one property of the object instead of the entire object.
Here is the code that Im working on:
const [movieList, setMovieList] = useState([]);
Calling the setMovieList and passing an array will obviously cause a rerender.
Consider the following contents of movieList:
movieList = [
{
'name': 'Mulholland Dr.'
'year':2001,
'watched' : true,
'rating':0
},
{
'name': 'Zodiac'
'year':2007,
'watched' : false,
'rating':0
},
{
'name': 'Twin Peaks'
'year':2017,
'watched' : true,
'rating': 0
}]
Then I have a function which renders the list:
function showMovieList () {
return movieList.map((movie) => {
return (
<List.Item key={movie.imdbID}>
<div className="watchedCheckBoxContainer">
<input type="checkbox" onChange={(event) => movie.watched = event.target.checked} id={`cb1${movie.imdbID}`}/>
<label htmlFor={`cb1${movie.imdbID}`}><Image size='tiny' src={movie.Poster} /></label>
</div>
{/* <Image size='tiny' src={movie.Poster} /> */}
<List.Content>{movie.Title}</List.Content>
{movie.watched ? <Rating maxRating={5} onRate={(event, {rating}) => movie.userRating=rating}/> : null}
</List.Item>
)
});
}
As you can see , when the checkbox is clicked it changes the value of the watched property. A few lines later I'm checking if movie.watched == true then show the <Rating> component. But in this case, I'm not using setMoviesList to update the moviesList and hence the <Rating> component is not visible.
How can I use setMoviesList to update watched property of the particular movie whose checkbox I click on?
Okay.. I solved it by the following way:
function onMovieWatched (watched, index) {
const tempMoviesList = [...movieList];
tempMoviesList[index].watched = watched;
setMovieList(tempMoviesList);
}
<input type="checkbox" onChange={(event) => onMovieWatched(event.target.checked, idx)} id={`cb1${movie.imdbID}`}/>
The idx is the index that I am using from the map method.
Initially I was afraid that I might have to loop over the entire array and get the object that matches the imdbID and then update its property.
Luckily I have the index while mapping over it, so I just used that to directly retrieve the object.
Dont know why I didnt think of this solution before posting.

What's the best way to ensure no null values

This question may be more about opinion than fact, but I'm unsure so thought I'd ask.
I'm building some forms which will display data and allow edits, the field data comes from props (as a parent component is using a GraphQL query to pull a larger amount and pass to each child).
I'm finding some input data is evaluating to null (as it's not passed back from the query) which throws a warning as inputs don't like being assigned null values.
My question is, when passing these values, what's the cleanest way to run checks on each variable and assign an empty string if needed?
So far the two options i've tried are:
Conditionally assign each to the state object, but this feels clunky and is a lot of code:
const [state, setState] = useState({
telephone: props.telephone ? props.telephone : '',
nickname: props.nickname ? props.nickname : ''
etc...
});
Or to define a function which maps over props and checks values, before setting state:
useEffect( () => {
let state_arr = {};
Object.keys(props).map( (key) => {
if( !props[key] ) state_arr[key] = '';
else state_arr[key] = props[key];
} );
setState(state_arr);
}, [] )
Honestly this feels cleaner than the first option, but there are a number of places this will occur and to have to do this in each feels counter productive.
Any help/insight appreciated.
EDIT: It turns out OP is using Material UI for this..Meaning, the reason the input is showing a warning is due to Material UI using PropTypes. I suggested that OP create a wrapper for the <Input /> component and pass through all props. Inside of the wrapper component you can just do: <InputWrapper value={props.value || ""} {...rest} /> and this covers things..
Live Demo
InputWrapper:
import React from 'react';
import { Input } from '#material-ui/core';
export default function InputWrapper({ value, ...rest }) {
return <Input value={value || ""} {...rest} />
}
InputWrapper In Use:
import React, { useState, useEffect } from 'react';
import { render } from 'react-dom';
import InputWrapper from './InputWrapper.js';
function App(props) {
const [state, setState] = useState({});
useEffect(() => {
setState({
name: props.name,
age: props.age,
hairColor: props.hairColor,
})
}, [props.name, props.age, props.hairColor]);
const handleChange = (event, inputType) => {
setState({...state, [inputType]: event.target.value})
}
return(
<div>
{/* Shows that you can pass through native <Input /> props: */}
{/* state.name is null here! Warning is NOT thrown in the console! */}
<InputWrapper value={state.name} fullWidth onChange={e => setState({...state, name: e.target.value})} />
<InputWrapper value={state.name} multiline onChange={e => setState({...state, name: e.target.value})} />
{Object.keys(state).map((item, index) => {
return (
<div>
<InputWrapper
key={`${item}_${index}`}
value={state[item]}
onChange={e => handleChange(e, item)} />
</div>
);
})}
</div>
);
}
render(
<App name={null} age={44} hairColor="blue" />,
document.getElementById('root')
);
ORIGINAL ANSWER:
What is your use case? There is no reason to run checks and assign empty strings...
If you are trying to enforce that certain properties are used, please look into PropTypes... If you are not wanting to enforce that certain props get used, I would recommend checking for a value during use of the variable. Even if you set it to an empty string initially, you could still encounter errors down the line - I don't understand what you gain from an empty string.
I don't understand the use case - can you elaborate more on why you need to set it to an empty string?
If you really wanted to, you could verify like: useState({two: props.two || ""}) ...but it is still unnecessary..
// Notice how prop "two" is not being used..
function Test(props) {
const [state, setState] = React.useState({
one: props.one,
two: props.two
})
return(
<div>
<p>{state.one}</p>
<p>Even though <code>state.two</code> does not exist, there are no errors.. (at least for this demonstration)</p>
<input type="text" value={state.two} />
<input type="text" value={state.two} defaultValue={"default"} />
<p><i>If you really wanted to, you could verify like:</i><code>useState({two: props.two || ""})</code><i>...but it is still unnecessary..</i></p>
</div>
);
}
ReactDOM.render(<Test one="I AM ONE" />, document.body)
code {
margin: 0 10px;
padding: 3px;
color: red;
background-color: lightgray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
What about making method KickOutNullValues() which will do what you want and then you can reuse it everywhere you need. That would be more elegant.
This is a tough question, i don't know the right answer. You already tried two ways, the different way that I normally do is,
If you just want to get the display right, i would just do
<Telephone data={props.telephone} />,
const Telephone = ({data}) => { if (!data) return null }
I found this is to allow the child component to ensure the validity of this issue rather than sorting out the data in the parent API level.
Telephone.defaultProps = {
data: ''
}
This further ensures that if the data is null, it'll be reset to '' by the defaultProps
The reason I prefer this way most of time is that I don't really want to mess with the origin TRUTH of the API data.
Of course your ways might be better if you do want to ensure the data is valid at all time :)
Your code will start to have spaghetti-like qualities if you put the raw algorithm inside your callback. I recommend writing a function outside.
Your usage of Array#map is not correct, or rather you are using it in an unintended way. Array#map is used to construct an entirely new array. You are simulating Array#forEach. Also, you're performing a falsey conditional check. null is one of many values that are considered false in JavaScript. Namely, your pain points will probably be undefined, 0, and ''. If the only invalid return value is null, then check for null explicitly.
The enumerable that is for your intended use case is Array#reduce:
function nullValueReplacer(obj) {
return Object.entries(obj).reduce((newStateArr, [currentKey, currentValue]) => {
if (currentValue === null) {
newStateArr[currentKey] = ''
} else {
newStateArr[currentKey] = currentValue
}
return newStateArr
}, {});
}
As a side note, you might want to update your variable names. It's pretty deceptive that you have a variable called state_arr that is an object.
Array of objects - little fix
You should not use key with map..
think about this: (similar to yours)
useEffect(() => {
let state_arr = Object.keys(props).map(prop => prop ? {prop} : { prop: '' });
setState(state_arr);
}, [])
By using this code you make an array with object and have easy access for every item
In case there is no nickname it will look like that:
[{ telephone: '245-4225-288' }, { nickname: '' }]
What do you think?

Prevent rerender on function prop update

I have a form with several layers of child components. The state of the form is maintained at the highest level and I pass down functions as props to update the top level. The only problem with this is when the form gets very large (you can dynamically add questions) every single component reloads when one of them updates. Here's a simplified version of my code (or the codesandbox: https://codesandbox.io/s/636xwz3rr):
const App = () => {
return <Form />;
}
const initialForm = {
id: 1,
sections: [
{
ordinal: 1,
name: "Section Number One",
questions: [
{ ordinal: 1, text: "Who?", response: "" },
{ ordinal: 2, text: "What?", response: "" },
{ ordinal: 3, text: "Where?", response: "" }
]
},
{
ordinal: 2,
name: "Numero dos",
questions: [
{ ordinal: 1, text: "Who?", response: "" },
{ ordinal: 2, text: "What?", response: "" },
{ ordinal: 3, text: "Where?", response: "" }
]
}
]
};
const Form = () => {
const [form, setForm] = useState(initialForm);
const updateSection = (idx, value) => {
const { sections } = form;
sections[idx] = value;
setForm({ ...form, sections });
};
return (
<>
{form.sections.map((section, idx) => (
<Section
key={section.ordinal}
section={section}
updateSection={value => updateSection(idx, value)}
/>
))}
</>
);
};
const Section = props => {
const { section, updateSection } = props;
const updateQuestion = (idx, value) => {
const { questions } = section;
questions[idx] = value;
updateSection({ ...section, questions });
};
console.log(`Rendered section "${section.name}"`);
return (
<>
<div style={{ fontSize: 18, fontWeight: "bold", margin: "24px 0" }}>
Section name:
<input
type="text"
value={section.name}
onChange={e => updateSection({ ...section, name: e.target.value })}
/>
</div>
<div style={{ marginLeft: 36 }}>
{section.questions.map((question, idx) => (
<Question
key={question.ordinal}
question={question}
updateQuestion={v => updateQuestion(idx, v)}
/>
))}
</div>
</>
);
};
const Question = props => {
const { question, updateQuestion } = props;
console.log(`Rendered question #${question.ordinal}`);
return (
<>
<div>{question.text}</div>
<input
type="text"
value={question.response}
onChange={e =>
updateQuestion({ ...question, response: e.target.value })
}
/>
</>
);
};
I've tried using useMemo and useCallback, but I can't figure out how to make it work. The problem is passing down the function to update its parent. I can't figure out how to do that without updating it every time the form updates.
I can't find a solution online anywhere. Maybe I'm searching for the wrong thing. Thank you for any help you can offer!
Solution
Using Andrii-Golubenko's answer and this article React Optimizations with React.memo, useCallback, and useReducer I was able to come up with this solution:
https://codesandbox.io/s/myrjqrjm18
Notice how the console log only shows re-rendering of components that have changed.
Use React feature React.memo for functional components to prevent re-render if props not changed, similarly to PureComponent for class components.
When you pass callback like that:
<Section
...
updateSection={value => updateSection(idx, value)}
/>
your component Section will rerender each time when parent component rerender, even if other props are not changed and you use React.memo. Because your callback will re-create each time when parent component renders. You should wrap your callback in useCallback hook.
Using useState is not a good decision if you need to store complex object like initialForm. It is better to use useReducer;
Here you could see working solution: https://codesandbox.io/s/o10p05m2vz
I would suggest using life cycle methods to prevent rerendering, in react hooks example you can use, useEffect. Also centralizing your state in context and using the useContext hook would probably help as well.
laboring through this issue with a complex form, the hack I implemented was to use onBlur={updateFormState} on the component's input elements to trigger lifting form data from the component to the parent form via a function passed as a prop to the component.
To update the component's input elelment, I used onChange={handleInput} using a state within the compononent, which component state was then passed ot the lifting function when the input (or the component as a whole, if there's multiple input field in the component) lost focus.
This is a bit hacky, and probably wrong for some reason, but it works on my machine. ;-)

Map through an array and render a specified component in React

I have 2 almost identical components that map through an array of options and generate either checkboxes or radio buttons. The only difference between the components is the child component that is rendered (either a checkbox or a radio input).
I would like to reduce the duplicate code in these components but I'm unsure as to the best way to tackle it. I could consolidate the 2 components into a single component such as FormControlGroup and add more props to allow me to select if I want either checkboxes or radio buttons to be rendered e.g. checkboxOrRadio, but this would mean that if I added new variations of checkboxes or radio buttons such as CustomCheckbox I would have to keep expanding on the props of this component.
<FormControlGroup
label="Radio buttons"
name="test"
options=[{label: 'one', value: 1}, {label: 'two', value: 2}, {label: 'three', value: 3}]
checkboxOrRadio="radio"
/>
Is it possible (and sensible) to pass a component in as a prop and have the component rendered in the map function? If so, how would I do this? Or are there any more elegant solutions? That way I could pass in any component I want and it will be rendered and have the key, name, label, onChange, value, checked props passed to it.
CheckboxGroup.js
import React from 'react';
import CheckboxInput from './CheckboxInput';
const CheckboxGroup = ({ label, name, options, onChange, children }) => {
return (
<div className="form-group form-inline">
<span className="faux-label">{label}</span>
{children}
<div className="form-inline__field-container">
{options &&
options.map(option => (
<CheckboxInput
key={option.value}
name={name}
label={option.label}
onChange={onChange}
value={option.value}
checked={option.value}
/>
))
}
</div>
</div>
);
};
export default CheckboxGroup;
RadioGroup.js
import React from 'react';
import RadioInput from './RadioInput';
const RadioGroup = ({ label, name, options, onChange, children }) => {
return (
<div className="form-group form-inline">
<span className="faux-label">{label}</span>
{children}
<div className="form-inline__field-container">
{options &&
options.map(option => (
<RadioInput
key={option.value}
name={name}
label={option.label}
onChange={onChange}
value={option.value}
checked={option.value}
/>
))
}
</div>
</div>
);
};
export default RadioGroup;
It is absolutely sensible in certain cases to pass a component as a prop. In fact, it is a pattern used in many React libraries, including React Router, which allows you to pass a component prop to the Route component.
In your case, the render function of your FormControlGroup component might look something like this:
render({component, ...}) { // component is a prop
const InputComponent = component;
return (
... // outer divs
{ options.map(option =>
<InputComponent key={option.value} ... />
}
...
)
}
Then you would use it like this:
<FormControlGroup
label="Radio buttons"
name="test"
options=[{label: 'one', value: 1}, {label: 'two', value: 2}, {label: 'three', value: 3}]
component={CheckboxInput}
/>
Another option would be to create a new component that takes care of rendering the outer <div> elements and then map options to a list of input components in some outer component. That allows you to make fewer assumptions about what props any given input component should be expecting. Since you're already using children, you'd have to split it into two components. Here's one possibility for what that might look like:
const FormControlGroup = ({ label, children }) => {
return (
<div className="form-group form-inline">
<span className="faux-label">{label}</span>
{children}
</div>
);
};
const FormControlOptions = ({ children }) => {
return (
<div className="form-inline__field-container">
{children}
</div>
);
};
const SomeOuterComponent = ({ label, name, options, onChange }) => {
<FormControlGroup label={label}>
... // other children
<FormControlOptions>
{
options.map(option =>
<CheckboxInput
key={option.value}
name={name}
label={option.label}
onChange={onChange}
value={option.value}
checked={option.value}
/>
)
}
</FormControlOptions>
</FormControlGroup>
}
Of course, there are many ways to design this. The exact implementation you go with will depend on your use case.
Doesn't seem like a bad idea to me. It's definitely possible, maybe it's not the most elegant solution, but I don't currently see what's wrong with it.
I personally wouldn't change it into a FormControlGroup component because it makes it simpler in the code where you're using the component. So you can quickly know what is a CheckboxGroup vs RadioGroup.
Changing it would mean checking the checkboxOrRadio prop every time to see exactly what component you were changing.
I'd say wait, if you start to get more elements you want the exact same way then create a common component. But for just two components to share some code it isn't a big deal.

React pattern for List Editor Dialog

I'd like to know what's the best pattern to use in the following use case:
I have a list of items in my ItemList.js
const itemList = items.map((i) => <Item key={i}></Item>);
return (
<div>{itemList}</div>
)
Each of this Items has an 'EDIT' button which should open a dialog in order to edit the item.
Where should I put the Dialog code?
In my ItemList.js => making my Item.js call the props methods to open the dialog (how do let the Dialog know which Item was clicked? Maybe with Redux save the id of the item inside the STORE and fetch it from there?)
In my Item.js => in this way each item would have its own Dialog
p.s. the number of items is limited, assume it's a value between 5 and 15.
You got a plenty of options to choose from:
Using React 16 portals
This option let you render your <Dialog> anywhere you want in DOM, but still as a child in ReactDOM, thus maintaining possibility to control and easily pass props from your <EditableItem> component.
Place <Dialog> anywhere and listen for special app state property, if you use Redux for example you can create it, place actions to change it in <EditableItem> and connect.
Use react context to send actions directly to Dialog, placed on top or wherever.
Personally, i'd choose first option.
You can have your <Dialog/> as separate component inside application's components tree and let it to be displayed in a case if your application's state contains some property that will mean "we need to edit item with such id". Then into your <Item/> you can just have onClick handler that will update this property with own id, it will lead to state update and hence <Dialog/> will be shown.
UPDATED to better answer the question and more completely tackle the problem. Also, followed the suggestion by Pavlo Zhukov in the comment below: instead of using a function that returns functions, use an inline function.
I think the short answer is: The dialog code should be put alongside the list. At least, this is what makes sense to me. It doesn't sound good to put one dialog inside each item.
If you want to have a single Dialog component, you can do something like:
import React, { useState } from "react";
import "./styles.css";
const items = [
{ _id: "1", text: "first item" },
{ _id: "2", text: "second item" },
{ _id: "3", text: "third item" },
{ _id: "4", text: "fourth item" }
];
const Item = ({ data, onEdit, key }) => {
return (
<div key={key}>
{" "}
{data._id}. {data.text}{" "}
<button type="button" onClick={onEdit}>
edit
</button>
</div>
);
};
const Dialog = ({ open, item, onClose }) => {
return (
<div>
<div> Dialog state: {open ? "opened" : "closed"} </div>
<div> Dialog item: {JSON.stringify(item)} </div>
{open && (
<button type="button" onClick={onClose}>
Close dialog
</button>
)}
</div>
);
};
export default function App() {
const [isDialogOpen, setDialogOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const openEditDialog = (item) => {
setSelectedItem(item);
setDialogOpen(true);
};
const closeEditDialog = () => {
setDialogOpen(false);
setSelectedItem(null);
};
const itemList = items.map((i) => (
<Item key={i._id} onEdit={() => openEditDialog(i)} data={i} />
));
return (
<>
{itemList}
<br />
<br />
<Dialog
open={isDialogOpen}
item={selectedItem}
onClose={closeEditDialog}
/>
</>
);
}
(or check it directly on this CodeSandbox)

Categories