React Context re-render child components

756 Views Asked by At

Form component created

Usage


<Form
     initialValues={{
                title: '',
                name: ''
            }}
            onSubmit={formSubmit}
        >
            <Form.Field
                name="title"
            >
                <Input placeholder="Наименование" />
            </Form.Field>
            <Form.Field
                name="name"
            >
                <Input placeholder="Еще поле" />
            </Form.Field>
            <Button>Сохранить</Button>
        </Form>

Form Component


import { FormField } from './FormField';

import { FormProps, FormContextT } from './../../../types/form';

import './style.scss';

export const FormContext = React.createContext<FormContextT>({
    formData: {},
    handleFieldChange: () => {}
});

export const Form = (props: FormProps): JSX.Element => {

        const { children, initialValues, className, onSubmit } = props;
        
        const [ formData, setFormData] = useState(initialValues);
        
        const handlerSubmit = (e: React.SyntheticEvent): void => {
            e.preventDefault();
            
            onSubmit();
        }
    
        const handleFieldChange = (name: string, value: string) => {
            setFormData({
                ...formData,
                [name]: value
            });
        }
    
        const context:FormContextT = {
            formData,
            handleFieldChange
        }
    
        return (
            <FormContext.Provider value={context}>
                <form 
                    onSubmit={handlerSubmit}
                >
                    {children}
                </form>
            </FormContext.Provider>
        )
    }
    
    Form.Field = FormField

FormField Component


import React, { useContext } from 'react';
import { FormContext } from './Form';
import { FormFieldProps } from './../../../types/form';
export const FormField = (props: FormFieldProps): JSX.Element => {

    const { name, children } = props;
    
    const formContext = useContext(FormContext);
    const { formData, handleFieldChange } = formContext;
    
    const fieldChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        handleFieldChange(name, e.target.value);
    
        if('onChange' in children.props) {
            children.props.onChange(e);
        }
    }
    
    console.log('re-render: ' + name);
    
    return (
        React.cloneElement(children, {
            name,
            onChange: fieldChange,
            value: formData[name]
        })
    );

}

Input Component


export const Input = (props: PropsTypes) => {

    const { className, value, name, placeholder, disabled, onChange, type = 'text' } = props;
    
    console.log('re-render-input: ' + name);
    
    return (
        <input
            type={type}
            name={name!}
            value={value!}
            placeholder={placeholder!}
            disabled={disabled!}
            onChange={onChange!}
        />
    )

}

All form field values are written to the formData object in the format {field: value} in the Form component and passed through the React Context to the FormField. The problem is that when the field value is overwritten, the formData in the parent component is updated, and all child FormFields are re-rendered. Tell me how to make it so that only the child that has been changed is re-rendered.

Tried to use the UseMemo hook but couldn't apply it correctly.

1

There are 1 best solutions below

0
Alissa On

there are a few things here

  1. FormField is using the whole form data, so even if you found a way to re-render only the components whose data has changed they would still re-render - they need to get only their own value.

  2. if the parent component is re-rendered ALL its children are re-rendered. you can wrap them with React.memo but memoization comes with a price.

  3. React looks at the context value as a single value, and compares it by reference. so

  • the value should be wrapped in useMemo otherwise the context value will change each time the component it's created in re-renders for any reason.
const context:FormContextT = React.useMemo(() = ({
           formData,
           handleFieldChange
       }), [formData, handleFieldChange]);
  • all components using a context value are re-rendered each time anything in the context value is changed (the reference changes) and React.memo won't help here - docs
  1. Why did you decide to use context? the fields are direct children of the form.
  2. Why is it important that they don't re-render? is there a lot of them? is there a lot of logic in their render function? if not, just let them re-render ( you can change the input so it will save the value only on blur and not each new letter )

Possible solutions (assuming it's important that fields won't re-render):

  1. don't use context, pass only the field value to the form fields and wrap the fields with React.memo.

  2. create a small external store with the form values and expose only an API (useValue(fieldId) and setValue(fieldId, value)). here is a sandbox with this solution