Keeping state of variable mapped from props in functional react component after component redraw - reactjs

Recently I started learning react and I decided to use in my project functional components instead of class-based. I am facing an issue with keeping state on one of my components.
This is generic form component that accepts array of elements in order to draw all of necessary fields in form. On submit it returns "model" with values coming from input fields.
Everything working fine until I added logic for conditionally enabling or disabling "Submit" button when not all required fields are set. This logic is fired either on component mount using useEffect hook or after every input in form input. After re-render of the component (e.g. conditions for enabling button are not met, so button becomes disabled), component function is fired again and my logic for creating new mutable object from passed props started again, so I am finished with empty object.
I did sort of workaround to make a reference of that mutated object outside of scope of component function, but i dont feel comfortable with it. I also dont want to use Redux for that simple sort of state.
Here is the code (I am using Type Script):
//component interfaces:
export enum FieldType {
Normal = "normal",
Password = "password",
Email = "email"
}
export interface FormField {
label: string;
displayLabel: string;
type: FieldType;
required: boolean;
}
export interface FormModel {
model: {
field: FormField;
value: string | null;
}[]
}
export interface IForm {
title: string;
labels: FormField[];
actionTitle: string;
onSubmit: (model: FormModel) => void;
}
let _formState: any = null;
export function Form(props: IForm) {
let mutableFormModel = props.labels.map((field) => { return { field: field, value: null as any } });
//_formState keeps reference outside of react function scope. After coponent redraw state inside this function is lost, but is still maintained outside
if (_formState) {
mutableFormModel = _formState;
} else {
_formState = mutableFormModel;
}
const [formModel, setFormModel] = useState(mutableFormModel);
const [buttonEnabled, setButtonEnabled] = useState(false);
function requiredFieldsCheck(formModel: any): boolean {
let allRequiredSet = true;
formModel.model.forEach((field: { field: { required: any; }; value: string | null; }) => {
if (field.field.required && (field.value === null || field.value === '')) {
allRequiredSet = false;
}
})
return allRequiredSet;
}
function handleChange(field: FormField, value: string) {
let elem = mutableFormModel.find(el => el.field.label === field.label);
if (elem) {
value !== '' ? elem.value = value as any : elem.value = null;
}
let submitEnabled = requiredFieldsCheck({ model: mutableFormModel });
setFormModel(mutableFormModel);
setButtonEnabled(submitEnabled);
}
useEffect(() => {
setButtonEnabled(requiredFieldsCheck({ model: mutableFormModel }));
}, [mutableFormModel]);
function onSubmit(event: { preventDefault: () => void; }) {
event.preventDefault();
props.onSubmit({ model: formModel })
}
return (
<FormStyle>
<div className="form-container">
<h2 className="form-header">{props.title}</h2>
<form className="form-content">
<div className="form-group">
{props.labels.map((field) => {
return (
<div className="form-field" key={field.label}>
<label>{field.displayLabel}</label>
{ field.type === FieldType.Password ?
<input type="password" onChange={(e) => handleChange(field, e.target.value)}></input> :
<input type="text" onChange={(e) => handleChange(field, e.target.value)}></input>
}
</div>
)
})}
</div>
</form>
{buttonEnabled ?
<button className={`form-action btn btn--active`} onClick={onSubmit}> {props.actionTitle} </button> :
<button disabled className={`form-action btn btn--disabled`} onClick={onSubmit}> {props.actionTitle} </button>}
</div>
</FormStyle >
);
}

So there is quite a lot going on with your state here.
Instead of using a state variable to check if your button should be disabled or not, you could just add something render-time, instead of calculating a local state everytime you type something in your form.
So you could try something like:
<button disabled={!requiredFieldsCheck({ model: formModel })}>Click me</button>
or if you want to make it a bit cleaner:
const buttonDisabled = !requiredFieldsCheck({model: formModel});
...
return <button disabled={buttonDisabled}>Click me</button>
If you want some kind of "caching" without bathering with useEffect and state, you can also try useMemo, which will only change your calculated value whenever your listeners (in your case the formModel) have changes.
const buttonDisabled = useMemo(() => {
return !requiredFieldsCheck({model: formModel});
}, [formModel]);

In order to keep value in that particular case, I've just used useRef hook. It can be used for any data, not only DOM related. But thanks for all inputs, I've learned a lot.

Related

How does a parent know about the state of a list of child forms using formik?

I have a form consisting of a list of building materials which can be materials already registered for the construction and new materials:
The MaterialBar:
function MaterialBar({
materialBarId,
material,
onDelete,
onChange,
}: MaterialBarProps) {
const { values, handleChange } = useFormik<MaterialBar>({
initialValues: material,
onSubmit: console.log,
enableReinitialize: true,
});
const updateField = (event) => {
handleChange(event);
onChange(materialBarId, values);
};
return (
<DropdownWrapper>
<Dropdown
label="Material"
items={/* A list of available materials */}
selectedItem={values.material}
onSelect={updateField}
/>
.... Other fields
<TextField
name="amount"
id="material-amount"
label="Amount"
type="number"
onChange={updateField}
/>
<DeleteButton onClick={() => onDelete(materialBarId)}>
<img src={remove} />
</DeleteButton>
</DropdownWrapper>
);
}
The parent (ConstructionMaterials):
function ConstructionMaterials({
project,
materials,
attachMaterial,
}: ProjectMaterialsProps) {
const [allMaterials, setAllMaterials] = useState<IProjectMaterial[]>(
getProjectMaterialsFrom(project.materials)
);
const saveAllMaterials = () => {
allMaterials.forEach((newMaterial) => {
if (newMaterial.isNew) {
attachMaterial(newMaterial);
}
});
};
const updateNewMaterial = (
materialBarId: number,
updatedMaterial: MaterialBar
): void => {
const updatedList: IProjectMaterial[] = allMaterials.map((material) => {
if (material.projectMaterialId === materialBarId) {
return {
...material,
materialId: materials.find(
(currentMaterial) =>
currentMaterial.name === updatedMaterial.material
)?.id,
type: updatedMaterial.type,
length: updatedMaterial.length,
width: updatedMaterial.width,
value: updatedMaterial.amount,
};
}
return material;
});
setAllMaterials(updatedList);
};
// Adds a new empty material to the list
const addMaterial = (): void => {
setAllMaterials([
...allMaterials,
{
projectMaterialId: calculateMaterialBarId(),
projectId: project.id,
isNew: true,
},
]);
};
return (
<>
{allMaterials.map((material) => {
const materialBar: MaterialBar = {
material: material.name || "",
type: material.type || "",
amount: material.value || 0,
length: material.length || 0,
width: material.width || 0,
};
return (
<AddMaterialBar
key={material.projectMaterialId}
materialBarId={material.projectMaterialId!}
materials={materials}
onDelete={removeMaterial}
onChange={updateNewMaterial}
/>
);
})}
<Button onClick={() => saveAllMaterials()}>
{texts.BUTTON_SAVE_MATERIALS}
</Button>
</>
);
}
I have a hard time figuring out how to manage the list of materials. I use Formik (the useFormik hook) in the MaterialBar to take care of the values of each field.
My challenge is how to keep all the data clean and easily pass it between the components while knowing which materials are new and which already exist. If I just use Formik in the MaterialBar, then ConstructionMaterials does not know about the changes made in each field and it needs the updated data because it calls the backend with a "save all" action (the "Save"-buttons in the image should not be there, but they are my temporary fix).
To circumvent this, I also keep track of each material in ConstructionMaterials with the onChange on MaterialBar, but that seems redundant, since this is what Formik should take care of. I have also added a isNew field to the material type to keep track of whether it is new, so I don't two lists for existing and new materials.
I have had a look at FieldArray in ConstructionMaterials, but shouldn't Formik be used in the child, since the parent should just grab the data and send it to the API?
So: is there a clever way to handle a list of items where the parent can know about the changes made in the childs form, to make a bulk create request without the parent having to also keep track of all the objects in the children?
Sorry about the long post, but I don't know how to make it shorter without loosing the context.

autosuggest not showing item immediately

I am looking into fixing a bug in the code. There is a form with many form fields. Project Name is one of them. There is a button next to it.So when a user clicks on the button (plus icon), a popup window shows up, user enters Project Name and Description and hits submit button to save the project.
The form has Submit, Reset and Cancel button (not shown in the code for breviety purpose).
The project name field of the form has auto suggest feature. The code snippet below shows the part of the form for Project Name field.So when a user starts typing, it shows the list of projects
and user can select from the list.
<div id="formDiv">
<Growl ref={growl}/>
<Form className="form-column-3">
<div className="form-field project-name-field">
<label className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated custom-label">Project Name</label>
<AutoProjects
fieldName='projectId'
value={values.projectId}
onChange={setFieldValue}
error={errors.projects}
touched={touched.projects}
/>{touched.projects && errors.v && <Message severity="error" text={errors.projects}/>}
<Button className="add-project-btn" title="Add Project" variant="contained" color="primary"
type="button" onClick={props.addProject}><i className="pi pi-plus" /></Button>
</div>
The problem I am facing is when some one creates a new project. Basically, the autosuggest list is not showing the newly added project immediately after adding/creating a new project. In order to see the newly added project
in the auto suggest list, after creating a new project,user would have to hit cancel button of the form and then open the same form again. In this way, they can see the list when they type ahead to search for the project they recently
created.
How should I make sure that the list gets immediately updated as soon as they have added the project?
Below is how my AutoProjects component looks like that has been used above:
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import axios from "axios";
import { css } from "#emotion/core";
import ClockLoader from 'react-spinners/ClockLoader'
function escapeRegexCharacters(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Use your imagination to render suggestions.
const renderSuggestion = suggestion => (
<div>
{suggestion.name}, {suggestion.firstName}
</div>
);
const override = css`
display: block;
margin: 0 auto;
border-color: red;
`;
export class AutoProjects extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
projects: [],
suggestions: [],
loading: false
}
this.getSuggestionValue = this.getSuggestionValue.bind(this)
this.setAutoSuggestValue = this.setAutoSuggestValue.bind(this)
}
// Teach Autosuggest how to calculate suggestions for any given input value.
getSuggestions = value => {
const escapedValue = escapeRegexCharacters(value.trim());
if (escapedValue === '') {
return [];
}
const regex = new RegExp(escapedValue, 'i');
const projectData = this.state.projects;
if (projectData) {
return projectData.filter(per => regex.test(per.name));
}
else {
return [];
}
};
// When suggestion is clicked, Autosuggest needs to populate the input
// based on the clicked suggestion. Teach Autosuggest how to calculate the
// input value for every given suggestion.
getSuggestionValue = suggestion => {
this.props.onChange(this.props.fieldName, suggestion.id)//Update the parent with the new institutionId
return suggestion.name;
}
fetchRecords() {
const loggedInUser = JSON.parse(sessionStorage.getItem("loggedInUser"));
return axios
.get("api/projects/search/getProjectSetByUserId?value="+loggedInUser.userId)//Get all personnel
.then(response => {
return response.data._embedded.projects
}).catch(err => console.log(err));
}
setAutoSuggestValue(response) {
let projects = response.filter(per => this.props.value === per.id)[0]
let projectName = '';
if (projects) {
projectName = projects.name
}
this.setState({ value: projectName})
}
componentDidMount() {
this.setState({ loading: true}, () => {
this.fetchRecords().then((response) => {
this.setState({ projects: response, loading: false }, () => this.setAutoSuggestValue(response))
}).catch(error => error)
})
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
// Autosuggest will call this function every time you need to update suggestions.
// You already implemented this logic above, so just use it.
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: this.getSuggestions(value)
});
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
};
render() {
const { value, suggestions } = this.state;
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: value,
value,
onChange: this.onChange
};
// Finally, render it!
return (
<div>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
/>
<div className="sweet-loading">
<ClockLoader
css={override}
size={50}
color={"#123abc"}
loading={this.state.loading}
/>
</div>
</div>
);
}
}
The problem is you only call the fetchRecord when component AutoProjects did mount. That's why whenever you added a new project, the list didn't update. It's only updated when you close the form and open it again ( AutoProjects component mount again)
For this case I think you should lift the logic of fetchProjects to parent component and past the value to AutoProjects. Whenever you add new project you need to call the api again to get a new list.

React: useState filter array not updating state

Edit:
My error occured because I passed an array as a second parameter to useEffect. Even though the values inside the array stayed the same, the reference changed constantly, therefore useEffect was called constantly and reset my checkbox values. That array was created by an useState call. I replaced useState by useReducer (reducer only changes the object reference if the object is actually changed) and updated some missing dependencies higher up the component tree.
Original question:
I have trouble updating a state in a functional component.
My question is somewhat similiar to this one:
React SetState doesn't call render
I'm already copying my state object (by using array.filter) instead of referencing it; but my state still doesn't update.
In order to track down the problem, I tried re-creating the problem in a minimal example:
jsfiddle
But in my minimal example, everything works as expected. I'm unable to reproduce the error.
Here is my example where the state doesn't update:
configCheckboxGroup.tsx:
import classNames from "classnames";
import React, { useState, useEffect } from "react";
import { Component } from "../../model";
import CheckboxPanel from "./panels/checkboxPanel";
interface configCheckboxGroupProps {
className?: string;
choices: Array<Component>;
selected: Array<string>;
addToCart: (items: Array<Component>) => void;
}
const ConfigCheckboxGroup: React.SFC<configCheckboxGroupProps> = ({
className,
choices,
selected,
addToCart,
}) => {
const [ selectedComp, setSelectedComp ] = useState<Array<string>>(selected);
// device loads later, selected has to be updated
useEffect(() => {
setSelectedComp(selected);
}, [selected]);
const handleOnChange = (ev: React.FormEvent, id: string) => {
console.debug(id);
console.debug(selectedComp.filter(el => el !== id));
if (selectedComp.includes(id)) {
// was already checked || this line is not working!
setSelectedComp(selectedComp.filter(el => el !== id));
} else {
// was not checked
setSelectedComp([...(selectedComp), id]);
}
const selected = choices.filter(el => selectedComp.includes(el.reference._id));
addToCart(selected);
};
return (
<div className={classNames("panellist", className)}>
{
choices.map(el => {
return (
<CheckboxPanel
image={ el.reference.picture ? el.reference.picture : undefined }
name={ el.reference.name }
id={ el.reference._id }
price={ el.reference.price ? el.reference.price :
el.price ? el.price : 0 }
key={ el._id }
checked={ selectedComp.includes(el.reference._id) }
onChange={ handleOnChange }
/>
)
})
}
<span>
{ selectedComp }
</span>
</div>
)
}
export default ConfigCheckboxGroup;
And checkboxPanel.tsx:
import classNames from "classnames";
import React from "react";
import "./checkboxPanel.scss";
import noImage from "../../../resources/images/errors/no-image.svg";
interface PanelProps {
className?: string;
image?: string;
name: string;
id: string;
price: number;
checked: boolean;
onChange: (ev: React.FormEvent, id: string) => void;
}
const CheckboxPanel: React.SFC<PanelProps> = ({
className,
image,
name,
id,
price,
checked,
onChange,
}) => {
const getImage = () => {
if (image) {
return image;
} else {
return noImage;
}
}
return (
<div className={classNames("panel", "checkbox-panel", className)}>
<div className="top">
<div className="image">
<img alt="Product" src={getImage()} />
</div>
<div className="name">
{name}
</div>
</div>
<div className="bottom">
<div className="category">
{ Number(price).toFixed(2) } €
</div>
<div>
<input type="checkbox"
checked={ checked }
onChange={ (e) => onChange(e, id) }
/>
</div>
</div>
</div>
)
};
export default CheckboxPanel;
The only difference between the examples is that in the second one, I call the handle function inside a child component. But I do the same thing on other occasions as well: I have a very similar Component configRadioGroup with radio buttons instead of checkboxes where everything works fine.
I tried playing around by manually filtering the array and trying a lot of other things, but nothing seemed to help. This is why, as a last try, I ask here (although I know that this question is not a good one due to it being very specific).
Changing the prop selected will reset selectedComp if you put a console log in your useEffect you may find that that is resetting it every time.
You need to track down where selected comes from (redux?) and how it's set (addToCart?).
A dirty fix could be to only set selectedComp when component mounts, this is dirty and will/should cause react-hooks/exhaustive-deps lint to trigger:
useEffect(() => {
setSelectedComp(selected);
}, []);
But better to track down what's going wrong with selected, if it comes from redux then maybe just use selected instead and forget about selectedComp since that is just a copy.

React/Redux controlled input with validation

Lets imagine we want an input for a "product" (stored in redux) price value.
I'm struggle to come up with the best way to handle input constraints. For simplicity, lets just focus on the constraint that product.price cannot be empty.
It seems like the 2 options are:
1: Controlled
Implementation: The input value is bound to product.price. On change dispatches the changePrice() action.
The main issue here is that if we want to prevent an empty price from entering the product store, we essentially block the user from clearing the input field. This isn't ideal as it makes it very hard to change the first digit of the number (you have to select it and replace it)!
2: Using defaultValue
Implementation: We set the price initially using input defaultValue, that allows us to control when we want to actually dispatch changePrice() actions and we can do validation handling in the onChange handler.
This works well, unless the product.price is ever updated from somewhere other than the input change event (for example, an applyDiscount action). Since defaultValue doesn't cause rerenders, the product.price and the input are now out of sync!
So what am I missing?
There must be a simple & elegant solution to this problem but I just can't seem to find it!
What I have done in the past is to use redux-thunk and joi to solve input constraints/validation using controlled inputs.
In general I like to have one update action that will handle all the field updating. So for example if you have two inputs for a form, it would looks something like this:
render() {
const { product, updateProduct } = this.props;
return (
<div>
<input
value={product.name}
onChange={() => updateProduct({...product, name: e.target.value})}
/>
<input
value={product.price}
onChange={() => updateProduct({...product, price: e.target.value})}
/>
</div>
)
}
Having one function/action here simplifies my forms a great deal. The updateProject action would then be a thunk action that handles side effects. Here is our Joi Schema(based off your one requirement) and updateProduct Action mentioned above. As a side note, I also tend to just let the user make the mistake. So if they don't enter anything for price I would just make the submit button inactive or something, but still store away null/empty string in the redux store.
const projectSchema = Joi.object().keys({
name: Joi.number().string(),
price: Joi.integer().required(), // price is a required integer. so null, "", and undefined would throw an error.
});
const updateProduct = (product) => {
return (dispatch, getState) {
Joi.validate(product, productSchema, {}, (err, product) => {
if (err) {
// flip/dispatch some view state related flag and pass error message to view and disable form submission;
}
});
dispatch(update(product)); // go ahead and let the user make the mistake, but disable submission
}
}
I stopped using uncontrolled inputs, simply because I like to capture the entire state of an application. I have very little local component state in my projects. Keep in mind this is sudo code and probably won't work if directly copy pasted. Hope it helps.
So I think I've figure out a decent solution. Basically I needed to:
Create separate component that can control the input with local state.
Pass an onChange handler into the props that I can use to dispatch my changePrice action conditionally
Use componentWillReceiveProps to keep the local value state in sync with the redux store
Code (simplified and in typescript):
interface INumberInputProps {
value: number;
onChange: (val: number) => void;
}
interface INumberInputState {
value: number;
}
export class NumberInput extends React.Component<INumberInputProps, INumberInputState> {
constructor(props) {
super(props);
this.state = {value: props.value};
}
public handleChange = (value: number) => {
this.setState({value});
this.props.onChange(value);
}
//keeps local state in sync with redux store
public componentWillReceiveProps(props: INumberInputProps){
if (props.value !== this.state.value) {
this.setState({value: props.value});
}
}
public render() {
return <input value={this.state.value} onChange={this.handleChange} />
}
}
In my Product Component:
...
//conditionally dispatch action if meets valadations
public handlePriceChange = (price: number) => {
if (price < this.props.product.standardPrice &&
price > this.props.product.preferredPrice &&
!isNaN(price) &&
lineItem.price !== price){
this.props.dispatch(updatePrice(this.props.product, price));
}
}
public render() {
return <NumberInput value={this.props.product.price} onChange={this.handlePriceChange} />
}
...
What i would do in this case is to validate the input onBlur instead of onChange.
For example consider these validations in the flowing snippet:
The input can't be empty.
The input should not contain "foo".
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
myVal: '',
error: ''
}
}
setError = error => {
this.setState({ error });
}
onChange = ({ target: { value } }) => {
this.setState({ myVal: value })
}
validateInput = ({ target: { value } }) => {
let nextError = '';
if (!value.trim() || value.length < 1) {
nextError = ("Input cannot be empty!")
} else if (~value.indexOf("foo")) {
nextError = ('foo is not alowed!');
}
this.setError(nextError);
}
render() {
const { myVal, error } = this.state;
return (
<div>
<input value={myVal} onChange={this.onChange} onBlur={this.validateInput} />
{error && <div>{error}</div>}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Edit
As a followup to your comments.
To make this solution more generic, i would pass the component a predicate function as a prop, only when the function will return a valid result i would call the onChange that passed from the parent or whatever method you pass that updating the store.
This way you can reuse this pattern in other components and places on your app (or even other projects).

Updating a selected object when user changes inputs in a form

When a user clicks a square it becomes currentHtmlObject. I want people to be able to update it's properties in the right sidebar.
I have no idea how to take a single input field and update an object's property that I'm holding in a react-redux state and update the main viewing area DrawingCanvas.
I got kinda close where the info I was entering into the form was activating my reducers and actions. But I couldn't figure out how to distinguish between left and top.
// React
import React from 'react'
export class RightSidebar extends React.Component {
constructor(props) {
super(props)
}
handleChange(evt) {
console.log(evt)
this.props.onUpdateCurrentHtmlObject(evt.target.value)
}
render() {
const { currentHtmlObject } = this.props
return (
<form>
{this.props.currentHtmlObject.id}
<div className="right-sidebar">
<div className="form-group">
<label>Position X</label>
<input
type="number"
name="left"
className="form-control"
value={this.props.currentHtmlObject.styles ? this.props.currentHtmlObject.styles.left : ''}
onChange={this.handleChange.bind(this)} />
</div>
<div className="form-group">
<label>Position Y</label>
<input
type="number"
className="form-control"
value={this.props.currentHtmlObject.styles ? this.props.currentHtmlObject.styles.top : ''}
onChange={this.handleChange.bind(this)} />
</div>
</div>
</form>
)
}
}
RightSidebar.defaultProps = {
currentHtmlObject: {
styles: {
left: null,
top: null
}
}
}
There is no need to distinguish between left and top, let's assume you have an action named update and all it does is to update a selected object's property. Here is what the action method may look like:
updateSelectedObj(id, payload){
return {
type: UPDATE_SELECTED_OBJ,
id: id,
payload: payload
}
}
Here is what your event handler might look like in class RightSidebar:
handleChange(evt) {
// since your top and left input fields have a corresponding name property, evt.target.name will return either `left` or `top`
store.dispatch(updateSelectedObj({styles:{evt.target.name:evt.target.value}})
}
Here is your reducer:
[UPDATE_SELECTED_OBJ]: (state, action) => {
// I assume you have a list of objects in the canvas but only one can
// be selected at a time. and I assume the name of the list is objList
let selectedObj = state.objList.filter(obj => obj.id == action.id)[0]
selectedObj = Object.assign({}, selectedObj, action.payload)
return { objList: state.objList.map(obj => obj.id === action.id? Object.assign({}, obj, selectedObj : obj) }
}
I might suggest simplifying the component itself. Sorry for being brief :). I can update w/ more context when I get some time.
This is a stripped down example, but basically thinking of each "number input" as only needing a value and onChange (emits value, not an event).
You would make use of react-redux's connect so that updateObject is a callback accepting the "patch data" to be merged into the currentObject's state.
/**
* #param currentObject
* #param updateObject An already bound action creator that accepts payload to "update object"
*/
function SideBar({currentObject, updateObject}) {
const {styles} = currentObject;
return (
<div>
<NumberInput
value={styles.left}
onChange={left => updateObject({left})}
/>
<NumberInput
value={styles.top}
onChange={top => updateObject({top})}
/>
</div>
)
}
The connect statement might look something like
const SideBarContainer = connect(
(state, {objectId}) => ({
currentObject: _.find(state.objects, {id}),
}),
(dispatch, {objectId}) => ({
updateObject: data => dispatch(
actions.updateObject(objectId, data)
)
})
)(SideBar);
And the actual usage, maybe something like
<SidebarContainer objectId={currentObjectId} />

Resources