Blaclittle
Blaclittle

Reputation: 127

how cache react prop to ref?

My component hava a onChange prop,
I don't want onChange fire useEffect,
so I want cache onChange to a ref,
below is my code,i don't know the code is right or not.

const Component = ({onChange})=>{
    const onChangeRef = useRef(onChange);
    onChangeRef.current = onChange;
    
    const [state, setState] = useState();

    useEffect(()=>{
        onChangeRef.current();
    },[state]);

    //....some code
}

Upvotes: 1

Views: 428

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074495

I don't want onChange fire useEffect

You can't do that in quite the way you've shown because by the time your function component is called, it's already in the process of rendering. It has to be memo-ized to prevent that using a "Higher Order Component." You could do just the memo-izing via React.memo with a custom areEqual callback, but your component would use a stale copy of the onChange prop. Instead, let's write our own siple HOC that provides a stable onChange in place of an unstable onChange.

function withStableOnChange(WrappedComponent) {
    let latestProps;

    // Our stable onChange function -- not an arrow function so that
    // it can pass on the `this` it's called with
    const onChange = function(...args) {
        return latestProps.onChange.apply(this, args);
    };

    // use `React.memo` to memo-ize the component:
    return React.memo(
        (props) => {
            latestProps = props; // Need this for the first render to work
            return <WrappedComponent {...props} onChange={onChange} />;
        },
        (prevProps, nextProps) => {
            // Remember the latest props, since we may not render
            latestProps = nextProps;

            // Props "are equal" if all of them are the same other than `onChange`
            const allPropNames = new Set([
                ...Object.keys(prevProps),
                ...Object.keys(nextProps),
            ]);
            for (const name of allPropNames) {
                if (name !== "onChange" && prevProps[name] !== nextProps[name]) {
                    return false; // Not equal
                }
            }
            return true; // Props are "equal"
        }
    );
}

Here's a live example. Notice that when you click the + in the first one ('Using component "without wrapper"'), the child component gets re-rendered because the parent component's onChange is unstable. But when you click the + in the second one ('Using component "with wrapper"'), the child-component doesn't get re-rendered because withStableOnChange memo-izes it and provides it with a stable version of onChange instead of the unstable one it receives.

const { useState, useEffect } = React;

function withStableOnChange(WrappedComponent) {
    let latestProps;

    // Our stable onChange function -- not an arrow function so that
    // it can pass on the `this` it's called with
    const onChange = function(...args) {
        return latestProps.onChange.apply(this, args);
    };

    // use `React.memo` to memo-ize the component:
    return React.memo(
        (props) => {
            latestProps = props; // Need this for the first render to work
            return <WrappedComponent {...props} onChange={onChange} />;
        },
        (prevProps, nextProps) => {
            // Remember the latest props, since we may not render
            latestProps = nextProps;

            // Props "are equal" if all of them are the same other than `onChange`
            const allPropNames = new Set([
                ...Object.keys(prevProps),
                ...Object.keys(nextProps),
            ]);
            for (const name of allPropNames) {
                if (name !== "onChange" && prevProps[name] !== nextProps[name]) {
                    return false; // Not equal
                }
            }
            return true; // Props are "equal"
        }
    );
}

const ComponentWithoutWrapper = ({onChange, componentType}) => {
    const [state, setState] = useState("");

    useEffect(() => {
        onChange(state);
    }, [state]);

    console.log(`Re-rendering the component "${componentType}"`);
    return (
        <input type="text" value={state} onChange={({currentTarget: {value}}) => setState(value)} />
    );
};

const ComponentWithWrapper = withStableOnChange(ComponentWithoutWrapper);

const Test = ({componentType, ChildComponent}) => {
    const [counter, setCounter] = useState(0);

    // This `onChange` changes on every render of `Example`
    const onChange = (value) => {
        // Using `counter` here to prove the up-to-date version of this function gets
        // when `ComponentWithWrapper` is used.
        console.log(`Got change event: ${value}, counter = ${counter}`);
    };

    return <div>
        <div>Using component "{componentType}":</div>
        <ChildComponent onChange={onChange} componentType={componentType} />
        <div>
            Example counter: {counter}{" "}
            <input type="button" onClick={() => setCounter(c => c + 1)} value="+" />
        </div>
    </div>;
};

const App = () => {
    // (Sadly, Stack Snippets don't support <>...</>)
    return <React.Fragment>
        <Test componentType="without wrapper" ChildComponent={ComponentWithoutWrapper} />
        <Test componentType="with wrapper"    ChildComponent={ComponentWithWrapper} />
    </React.Fragment>;
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

withStableOnChange is just an example. In a real HOC, you'd probably at least make the name of the prop a parameter, and maybe even make it possible to handle multiple function props rather than just one.


Just for completeness, here's a copy of withStableOnChange that uses a class component rather than React.memo. Both versions support using either class components or function components as the component they wrap, it's purely a matter of whether you write the HOC using a function component or a class component.

function withStableOnChange(WrappedComponent) {
    return class extends React.Component {
        constructor(props) {
            super(props);
            const instance = this;
            // Our stable onChange function -- not an arrow function so that
            // it can pass on the `this` it's called with
            this.onChange = function(...args) {
                return instance.props.onChange.apply(this, args);
            };
        }
        shouldComponentUpdate(nextProps, nextState) {
            // Component should update if any prop other than `onChange` changed
            const allPropNames = new Set([
                ...Object.keys(this.props),
                ...Object.keys(nextProps),
            ]);
            for (const name of allPropNames) {
                if (name !== "onChange" && this.props[name] !== nextProps[name]) {
                    return true; // Component should update
                }
            }
            return false; // No need for component to update
        }
        render() {
            return <WrappedComponent {...this.props} onChange={this.onChange} />;
        }
    };
}

That live example using this version of withStableOnChange instead:

const { useState, useEffect } = React;

function withStableOnChange(WrappedComponent) {
    return class extends React.Component {
        constructor(props) {
            super(props);
            const instance = this;
            // Our stable onChange function -- not an arrow function so that
            // it can pass on the `this` it's called with
            this.onChange = function(...args) {
                return instance.props.onChange.apply(this, args);
            };
        }
        shouldComponentUpdate(nextProps, nextState) {
            // Component should update if any prop other than `onChange` changed
            const allPropNames = new Set([
                ...Object.keys(this.props),
                ...Object.keys(nextProps),
            ]);
            for (const name of allPropNames) {
                if (name !== "onChange" && this.props[name] !== nextProps[name]) {
                    return true; // Component should update
                }
            }
            return false; // No need for component to update
        }
        render() {
            return <WrappedComponent {...this.props} onChange={this.onChange} />;
        }
    };
}

const ComponentWithoutWrapper = ({onChange, componentType}) => {
    const [state, setState] = useState("");

    useEffect(() => {
        onChange(state);
    }, [state]);

    console.log(`Re-rendering the component "${componentType}"`);
    return (
        <input type="text" value={state} onChange={({currentTarget: {value}}) => setState(value)} />
    );
};

const ComponentWithWrapper = withStableOnChange(ComponentWithoutWrapper);

const Test = ({componentType, ChildComponent}) => {
    const [counter, setCounter] = useState(0);

    // This `onChange` changes on every render of `Example`
    const onChange = (value) => {
        // Using `counter` here to prove the up-to-date version of this function gets
        // when `ComponentWithWrapper` is used.
        console.log(`Got change event: ${value}, counter = ${counter}`);
    };

    return <div>
        <div>Using component "{componentType}":</div>
        <ChildComponent onChange={onChange} componentType={componentType} />
        <div>
            Example counter: {counter}{" "}
            <input type="button" onClick={() => setCounter(c => c + 1)} value="+" />
        </div>
    </div>;
};

const App = () => {
    // (Sadly, Stack Snippets don't support <>...</>)
    return <React.Fragment>
        <Test componentType="without wrapper" ChildComponent={ComponentWithoutWrapper} />
        <Test componentType="with wrapper"    ChildComponent={ComponentWithWrapper} />
    </React.Fragment>;
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

Upvotes: 1

Related Questions