缓存如何对 ref 做出反应?

how cache react prop to ref?

我的组件有一个 onChange 属性,
我不想 onChange 触发 useEffect,
所以我想将 onChange 缓存到一个 ref,
下面是我的代码,不知道代码对不对。

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

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

    //....some code
}

I don't want onChange fire useEffect

你不能完全完全你所展示的方式,因为当你的函数组件被调用时,它已经 在渲染过程中。它必须是 memo-ized 以防止使用“Higher Order Component”。您可以使用自定义 areEqual 回调通过 React.memo just memo-izing,但您的组件将使用 [=18= 的陈旧副本] 道具。相反,让我们编写自己的简单 HOC,它提供稳定的 onChange 代替不稳定的 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"
        }
    );
}

这是一个活生生的例子。请注意,当您单击第一个('Using component "without wrapper"')中的 + 时,子组件会得到 re-rendered,因为父组件的 onChange 不稳定。但是,当您单击第二个 ('Using component "with wrapper"') 中的 + 时,child-component 不会得到 re-rendered 因为 withStableOnChange memo-izes 它和为它提供 onChange 的稳定版本,而不是它收到的不稳定版本。

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 只是一个例子。在真正的 HOC 中,您可能至少将 prop 的名称作为参数,甚至可能处理多个函数 props 而不是一个。


为了完整起见,这里有一个 withStableOnChange 的副本,它使用 class 组件而不是 React.memo。两个版本都支持使用 class 组件或函数组件作为它们包装的组件,这纯粹是您使用函数组件还是 class 组件编写 HOC 的问题。

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} />;
        }
    };
}

使用此版本 withStableOnChange 的实例:

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>