React design pattern dilemma - steppers in numeric form components - reactjs

So we all know uncontrolled components are usually a bad thing, which is why we usually want to manage the state of an input (or group of inputs) at a higher-level component, usually some kind of container. For example, a <Form /> component manages state and passes down state as values to its <Input /> components. It also passes down functions such as handleChange() that allow the Input to update the state.
But while implementing my own <NumericInput /> component, it got me thinking that fundamentally this component is not self-reliant. It's reusable but requires a lot of repetition (opposite of DRY mentality) because everywhere in my app that I want to use this component, I have to implement these state values, a handleChange function, and in the case of my <NumericInput />, two additional functions to control the stepper arrows.
If I (or someone who took over my code) wanted to use this <NumericInput />, but they forget to run to a different container component and copy the stepUp() and stepDown() functions to pass down as props, then there will just be two non-functional arrows. I understand that this model allows our components to be flexible, but they also seem to be more error-prone and dependent on other components elsewhere. Again, it's also repetitive. Am I thinking about this incorrectly, or is there a better way of managing this?
I recognize this is more of a theory/design question, but I'm including my code below for reference:
NumericInput:
const NumericInput = ({label, stepUp, stepDown, ...props}) => (
<>
{label && <Label>{label}</Label>}
<InputContainer>
<Input type={props.type || "number"} {...props} />
<StepUp onClick={stepUp}>
//icon will go here
</StepUp>
<StepDown onClick={stepDown}>
//icon will go here
</StepDown>
</InputContainer>
</>
);
Form.js
const Form = (props) => {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
}
const stepUp = () => {
setValue(value++);
}
const stepDown = () => {
setValue(value--);
}
return (
<NumericInput
stepUp={stepUp}
stepDown={stepDown}
handleChange={handleChange}
label="Numeric"
)
}

Let's try to boil down your questions a bit:
[NumericInput] is reusable but requires a lot of repetition (opposite of DRY mentality) because everywhere in my app that I want to use this component, I have to implement these state values, a handleChange function, and in the case of my , two additional functions to control the stepper arrows.
The only repetition you have is to define a value and a handleChange callback property for <NumericInput />. The value prop contains your numeric input value and is passed in from the parent container component owning that state. You need the callback to trigger a change request from the child (controlled component). That is the ususal approach, when you need to lift state up or divide your components in container and presentational components.
stepUp/stepDown methods are an implementation detail of NumericInput, so you can remove them from Form. Form just wants to know, if a value change has been triggered and what the new changed numeric value is.
Concerning where to store the state: There is probably a reason, why you want to save the state in the Form component. The user enters some input in (possibly) multiple fields before pressing the submit button. In this moment your form needs all the state of its child fields to trigger further processing based on the given input. And the idiomatic React way is to lift state up to the form to have the information.
I understand that this model allows our components to be flexible, but they also seem to be more error-prone and dependent on other components elsewhere. Again, it's also repetitive.
A presentational component is rather less error prone and is independent, because it doesn't care about state! For sure, you write a bit more boilerplate code by exposing callbacks and value props in child. The big advantage is, that it makes the stateless components far more predictable and testable and you can shift focus on complex components with state, e.g. when debugging.
In order to ease up props passing from Form to multiple child components, you also could consolidate all event handlers to a single one and pass state for all inputs. Here is a simple example, how you could handle the components:
NumericInput.js:
const NumericInput = ({ label, value, onValueChanged }) => {
const handleStepUp = () => {
onValueChanged(value + 1);
};
const handleStepDown = () => {
onValueChanged(value - 1);
};
return (
<>
...
<Input value={this.state.value} />
<StepUp onClick={handleStepUp}></StepUp>
<StepDown onClick={handleStepDown}></StepDown>
</>
);
};
Form.js:
const Form = (props) => {
const [value, setValue] = useState(0);
const handleValueChanged = (value) => {
setValue(value);
}
return (
<NumericInput
onValueChanged={handleValueChanged}
value={value}
label="Numeric"
)
}

Move the setValue function to the NumericInput component
Manage your state through the component.
return (
<NumericInput
setValue={setValue}
label="Numeric"
/>
)
I would recommend you use hooks

Related

ReactJS, passing data between siblings

Can someone, please, point me in the right direction?
I have 3 components in my app.jsfile:
componentA
componentB
componentC
I guess they are siblings, no parent/child relationships here.
Components A and B are 2 input forms.
Both inputs are for numbers (3 digit numbers).
Component C is the button.
Once I click on the button I need to get input values from components A and B (so would be 2 numbers), do some if/else statements and return some result.
My question is what's the best way to pass data from components A and B into the C?
As the React doc says, if two components need to have access to the same state, the solution is to put the state in the closest common ancestor.
In the case of your form, the button needs to read the states from each input to submit the form. So each input's state should be in the parent of the button and the two inputs.
Here is one example :
const Form = () => {
const [value1, setValue1] = useState('')
const [value2, setValue2] = useState('')
return (
<>
<Input value={value1} onChange={event => setValue1(event.target.value)}/>
<Input value={value2} onChange={event => setValue2(event.target.value}}/>
<SubmitButton onClick={() => submit({value1, value2})}/>
</>
)
}
If you need to pass input values through too many props, you can use a context, a state management library (zustand, jotai, ...) or a form library (Formik, react-hook-form, ..).

Approach for useEffect to avoid exhaustive-dep warning

I keep running into situations where the exhaustive-dep rule complains. In a simple below I want to reset the value whenever the props.id value changes:
const TestComponent = props => {
const [value, setValue] = useState(props.value);
useEffect(() => {
setValue(props.value);
}, [props.id]);
const saveValue = () => {
props.save(value);
}
return <input value={value} onChange={e => setValue(e.target.value)} onBlur={saveValue} />;
};
However, the lint rule wants me to also add props.value to the dependencies. If I do that it will effectively break the component since the effect will run on every change to the input and thus reset the state.
Is there a "right" way of achieving this behavior?
The way you're written your component is in a half controlled half uncontrolled style, which is difficult to do. I would recommend going either fully controlled, or fully uncontrolled.
A "controlled" component is one where it just acts on the props its given. It takes the value from its props and forwards it to the input, with no state. When the input changes, it calls an onChange prop. When the input blurs, it calls an onBlur prop. All the business logic is handled by the parent component.
The other option is an uncontrolled component, which is mostly what you have. The parent will pass in a prop for the initial value. I'd recommend naming it initialValue so it's clear this will only be checked once. After that, everything is managed by the child component. This means your useEffect goes away entirely:
const TestComponent = props = {
const [value, setValue] = useState(props.initialValue);
const saveValue = () => {
props.save(value);
}
return <input value={value} onChange={e => setValue(e.target.value)} onBlur={saveValue} />;
}
So how do you reset this then? With a key. The parent component can use a standard react key to tell react that it should unmount the old component and mount a new one:
const ParentComponent = props = {
// some code...
return <TestComponent key={id} initialValue={"whatever"} />
}
As long as the id doesn't change, the TestComponent will do its thing, starting with the initialValue passed in. When the id changes, the TestComponent unmounts and a new one mounts in its place, which will start with the initialValue it's given.

Dispatching actions from presenational components with regards to Container Component pattern?

I have just started to explore the Container Component pattern. What I don't quite grasp yet is the concept of presentational component only being concerned with the visuals.
Does that mean that these component can't dispatch action that would change the redux state?
e.g
<MainContainer>
<ListComponent />
<GraphComponent />
</MainContainer>
Say <GraphComponent> shows graphs based on a list in redux state. The <ListComponent> then modify this list in the redux state with buttons. Is this okay in the Container Component pattern?
I think that you're not supposed to dispatch actions in Components. In Container-Component pattern, you're supposed to pass a callback function from the container (MainContainer in your case) as props to ListComponent, that fires when the button is clicked and dispatch action (in container) as result.
Presentation (Dumb) components are like 1st grade students, they have their unique appearance but their behavior and the content they put out is decided or taught by their parents.
Example: <Button />
export const Button = props => {
<button type="button" onClick={props.onClick} />{props.text}</button>
}
Unique appearance: it's a button
Behavior: onClick
Content: text
Both onClick and text are provided by parent.
When they grow to 5th or 7th grade, they may start to have their own state and decide few of the things on their own.
Example: <Input />
class Input extends Component {
constructor(props) {
super(props);
this.state = {
value: ''
}
}
handleChange = (e) => {
this.setState({value: e.target.value});
}
render() {
return (
<input
type="text"
value={this.state.value}
onChange={this.handleChange}
onFocus={this.props.onFocus}
/>
);
}
}
Unique appearance: it's an input
State: decides it own value.
Behavior: onFocus
onFocus provided by parent.
And when they become adults they may start to behave on their own and may not
need their parents guidance. They start to talk to the outer world (redux store)
on their own (now they are new Container components).
Example
const mapStateToProps = (state, [ownProps]) => {
...
}
const mapDispatchToProps = (state, [ownProps]) => {
...
}
const MyComponent = (props) => {
return (
<ChildComponent {...props} />
);
}
connect(mapStateToProps, mapDispatchToProps)(MyComponent);
Decides its own behavior and content, may or may not need parent (read ownProps)
Presentational components may or may not need any behavior of their own,
they rely on their parents for that. But to talk (dispatch) to the outer
world(store) they need to be big enough (Container components).
So the main thing to remember here is start small and decide as your component
grows whether it needs to be a presentational or container component.

How to add context to recompose's withHandlers()?

recompose has this function called withHandlers that lets you define an event handler while keeping your component pure.
e.g. if you had something like this inside you render() method:
<TextInput value={value} onChange={ev => this.handleChange(ev.target.value)} />
it wouldn't be pure because every time your component renders, it'd by passing a different onChange function to your TextInput component.
This is great, but how can this be extended to support arrays of inputs? Or otherwise provide auxiliary data?
e.g. taking their example and extending it a bit:
const enhance = compose(
withState('values', 'updateValue', ''),
withHandlers({
onChange: props => inputName => event => {
props.updateValue(inputName, event.target.value)
},
onSubmit: props => event => {
event.preventDefault();
submitForm(props.value)
}
})
)
const Form = enhance(({ value, onChange, onSubmit, inputs }) =>
<form onSubmit={onSubmit}>
<label>Value
{inputs.map(input => (
<TextInput value={value} onChange={onChange(input)} />
))}
</label>
</form>
)
I've fudged the details a bit, but pretend inputs comes in as an array of input names. e.g. ["firstName","lastName"] would render two textboxes, one for each.
I want to store the values for each of these in my state, but I don't want to define separate updater functions for each. Thus I need to attach some metadata to the onChange={...} prop so that I know which field I'm updating in my state.
How can I do that?
In my example I wrote onChange(input) and added an extra 'level' to the withHandlers.onChange function to accept the extra argument, but withHandlers doesn't actually work that way. Is there some way to do this -- i.e., ensure that each TextInput receives the same function instance every time <Form> is rendered?
That's a typical case where you need to define the change handle directly inside your TextInput component.
You'd need to pass updateValue function as a prop to TextInput components.

How can I reset a react component including all transitively reachable state?

I occasionally have react components that are conceptually stateful which I want to reset. The ideal behavior would be equivalent to removing the old component and readding a new, pristine component.
React provides a method setState which allows setting the components own explicit state, but that excludes implicit state such as browser focus and form state, and it also excludes the state of its children. Catching all that indirect state can be a tricky task, and I'd prefer to solve it rigorously and completely rather that playing whack-a-mole with every new bit of surprising state.
Is there an API or pattern to do this?
Edit: I made a trivial example demonstrating the this.replaceState(this.getInitialState()) approach and contrasting it with the this.setState(this.getInitialState()) approach: jsfiddle - replaceState is more robust.
To ensure that the implicit browser state you mention and state of children is reset, you can add a key attribute to the root-level component returned by render; when it changes, that component will be thrown away and created from scratch.
render: function() {
// ...
return <div key={uniqueId}>
{children}
</div>;
}
There's no shortcut to reset the individual component's local state.
Adding a key attribute to the element that you need to reinitialize, will reload it every time the props or state associate to the element change.
key={new Date().getTime()}
Here is an example:
render() {
const items = (this.props.resources) || [];
const totalNumberOfItems = (this.props.resources.noOfItems) || 0;
return (
<div className="items-container">
<PaginationContainer
key={new Date().getTime()}
totalNumberOfItems={totalNumberOfItems}
items={items}
onPageChange={this.onPageChange}
/>
</div>
);
}
You should actually avoid replaceState and use setState instead.
The docs say that replaceState "may be removed entirely in a future version of React." I think it will most definitely be removed because replaceState doesn't really jive with the philosophy of React. It facilitates making a React component begin to feel kinda swiss knife-y.
This grates against the natural growth of a React component of becoming smaller, and more purpose-made.
In React, if you have to err on generalization or specialization: aim for specialization. As a corollary, the state tree for your component should have a certain parsimony (it's fine to tastefully break this rule if you're scaffolding out a brand-spanking new product though).
Anyway this is how you do it. Similar to Ben's (accepted) answer above, but like this:
this.setState(this.getInitialState());
Also (like Ben also said) in order to reset the "browser state" you need to remove that DOM node. Harness the power of the vdom and use a new key prop for that component. The new render will replace that component wholesale.
Reference: https://facebook.github.io/react/docs/component-api.html#replacestate
The approach where you add a key property to the element and control its value from the parent works correctly. Here is an example of how you use a component to reset itself.
The key is controlled in the parent element, but the function that updates the key is passed as a prop to the main element. That way, the button that resets a form can reside in the form component itself.
const InnerForm = (props) => {
const { resetForm } = props;
const [value, setValue] = useState('initialValue');
return (
<>
Value: {value}
<button onClick={() => { setValue('newValue'); }}>
Change Value
</button>
<button onClick={resetForm}>
Reset Form
</button>
</>
);
};
export const App = (props) => {
const [resetHeuristicKey, setResetHeuristicKey] = useState(false);
const resetForm = () => setResetHeuristicKey(!resetHeuristicKey);
return (
<>
<h1>Form</h1>
<InnerForm key={resetHeuristicKey} resetForm={resetForm} />
</>
);
};
Example code (reset the MyFormComponent and it's state after submitted successfully):
function render() {
const [formkey, setFormkey] = useState( Date.now() )
return <>
<MyFormComponent key={formkey} handleSubmitted={()=>{
setFormkey( Date.now() )
}}/>
</>
}
Maybe you can use the method reset() of the form:
import { useRef } from 'react';
interface Props {
data: string;
}
function Demo(props: Props) {
const formRef = useRef<HTMLFormElement | null>(null);
function resetHandler() {
formRef.current?.reset();
}
return(
<form ref={formRef}>
<input defaultValue={props.data}/>
<button onClick={resetHandler}>reset</button>
</form>
);
}

Resources