简体   繁体   English

如何从工厂 function 返回 React 记忆回调

[英]How to return a React memoized callback from factory function

I am wondering how best to return a memoized callback function in React when using a factory to generate that callback.我想知道在使用工厂生成回调时如何最好地在 React 中返回一个记忆化的回调 function。 The goal is to return the same function instance when the factory is called with the same parameter.目标是在使用相同参数调用工厂时返回相同的 function 实例。

For example:例如:

function MyComponent() {
    // This will always return a new function
    const makeCallback = param => () => {
        /* perform action with 'param' */
    };

    return (
        <>
            <Button onClick={makeCallback('foo')} />
            <Button onClick={makeCallback('bar')} />
            <Button onClick={makeCallback('baz')} />
        </>
    );

I don't believe wrapping the factory itself with a useCallback would provide any benefit, since that function is not actually being passed to any of the children, so my idea was to return a useCallback function from the factory.我不相信用 useCallback 包装工厂本身会带来任何好处,因为 function 实际上并没有传递给任何孩子,所以我的想法是从工厂返回 useCallback function。

Like this:像这样:

const makeCallback = param => React.useCallback(
    () => {
        /* perform action with 'param' */
    },
    [param]
);

However, that was not allowed and failed at build time.但是,这是不允许的,并且在构建时失败了。

React Hook "React.useCallback" is called in function "makeCallback" which is neither a React function component or a custom React Hook function - react-hooks/rules-of-hooks

The "Rules of Hooks" say clearly that calling a hook in a nested function is not permitted, but this seems odd to me since a custom hook is often literally just a function that calls other hooks. “钩子规则”清楚地表明,不允许在嵌套的 function 中调用钩子,但这对我来说似乎很奇怪,因为自定义钩子通常实际上只是一个调用其他钩子的 function。 It says the primary concern is preserving the order of execution, but I don't think that would be violated in this case.它说主要关注的是保持执行顺序,但我认为在这种情况下不会违反。

Is my best option to turn my factory into a hook and call it explicitly at the top level for each case?将我的工厂变成一个钩子并在每个案例的顶层明确调用它是我最好的选择吗? I'd prefer the simplicity of building the callback in the button itself, since it's a little less typing and the param piece is more apparent and obvious when kept with the button.我更喜欢在按钮本身中构建回调的简单性,因为它的输入少了一点,并且当与按钮保持在一起时,参数片段更加明显和明显。

// Same factory function, but now it's magically a "hook"
const useCallbackFactory = param => {
    return React.useCallback(() => { /* do 'param' stuff */ }, [param]);
}

function MyComponent() {
    // Define the callbacks ahead of time
    const fooCb = useCallbackFactory('foo');
    const barCb = useCallbackFactory('bar');
    const bazCb = useCallbackFactory('baz');

    return (
        <>
            <Button onClick={fooCb} />
            <Button onClick={barCb} />
            <Button onClick={bazCb} />
        </>
    );
}

The goal is to return the same function instance when the factory is called with the same parameter.目标是在使用相同参数调用工厂时返回相同的 function 实例。

I think what you want is something like this:我认为你想要的是这样的:

 let memoizedcb = React.useCallback(
    memoize((fieldName) => (val) => doSomething(fieldName, val)),
    []
  );

where在哪里

import memoize from "fast-memoize";

Now function returned by memoizedcb will be same across renders for the same argument.现在,由 memoizedcb 返回的memoizedcb将在相同参数的渲染中相同。

Now you can use it like现在你可以像这样使用它了

<TextField onChange={memoizedcb("name")} value={name}/>
<TextField onChange={memoizedcb("surname")} value={surname}}/>

One possible way of doing so is by creating a custom hook: (Don't take this example too seriously, just to make a point).一种可能的方法是创建一个自定义钩子:(不要太认真地对待这个例子,只是为了说明一点)。

const useFactory = (type) => {
    let f;
    if (type === 'a') {
    f = () => console.log('hello world');
  } else {
    f = () => console.log('bye world');
  }

  return React.useCallback(f, [type]);
};

const Button = React.memo(({ onClick, children }) => {
    console.log('re render');
    return <button onClick={onClick}>{children}</button>;
});

const App = () => {
  const f1 = useFactory('a');
  const f2 = useFactory('b');

  return (
    <div>
      <Button onClick={f1}>F1</Button>
      <Button onClick={f2}>F1</Button>
    </div>  
  );

};

https://jsfiddle.net/kadoshms/740zkeso/9/ https://jsfiddle.net/kadoshms/740zkeso/9/

I think you are close with useCallback but you really just want to memoize the result of the factory.我认为您与useCallback很接近,但您真的只想记住工厂的结果 The useMemo hook can do that. useMemo钩子可以做到这一点。

const callback = useMemo(() => {
  return callbackFactory(param);
}, [param]);

The hook won't recompute its return value (ie callback) unless the param in the dependency array changes.除非依赖数组中的param发生变化,否则钩子不会重新计算其返回值(即回调)。

Demo演示

const callbackFactory = param => {
  console.log("invoked callback factory!!");
  switch (param) {
    case "a":
      return () => console.log("callback A");

    case "b":
      return () => console.log("callback B");

    default:
      return () => {};
  }
};

export default function App() {
  const [param, setParam] = useState("a");
  const [, setCount] = useState(0);

const callback = useMemo(() => {
  return callbackFactory(param);
}, [param]);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>

      <select defaultValue="a" onChange={e => setParam(e.target.value)}>
        {["a", "b"].map(param => (
          <option key={param} value={param}>
            {param}
          </option>
        ))}
      </select>

      <button type="button" onClick={callback}>
        Press Me
      </button>
      <button type="button" onClick={() => setCount(c => c + 1)}>
        Rerender!
      </button>
    </div>
  );
}

从工厂编辑记忆回调

If the callbacks should remain the same between different "instances" of the component and remounts of the component it may make sense to declare them in advance out of the scope of the component.如果回调在组件的不同“实例”和组件的重新挂载之间应该保持相同,则提前在组件的 scope 之外声明它们可能是有意义的。

const cb1 = params => { ... };
const cb2 = params => { ... };

const App = () => (
  <div>
    <Button onClick={cb1}>F1</Button>
    <Button onClick={cb2}>F2</Button>
  </div>  
);

This is not very handy, so it may make sense to memoize the results factory returns and reuse it.这不是很方便,因此记住工厂返回的结果并重用它可能是有意义的。

 const makeMemoizedCallback = factory => { let cache = {}; return (...args) => { const key = JSON.stringify(args); if (.(key in cache)) { cache[key] = factory(..;args); } return cache[key]; }; }. const makeCallback = makeMemoizedCallback(param => runtime => { console,log(param; runtime); }). console;log(makeCallback("a") === makeCallback("a")); makeCallback("a")(1); makeCallback("b")(2);

Usage:用法:

const makeButtonClickHandler = makeMemoizedCallback(param => e => {
  console.log(param, e);
});

const App = () => (
  <div>
    <Button onClick={makeButtonClickHandler("F1")}>F1</Button>
    <Button onClick={makeButtonClickHandler("F2")}>F1</Button>
  </div>
);

This is very unfortunate, but react doesn't support something like this.这是非常不幸的,但是 react 不支持这样的东西。 The best way I found would be to reuse the same memoized callback and use name as the id, as follows (modifying your code snippet):我发现的最好方法是重用相同的记忆回调并使用name作为 id,如下(修改您的代码片段):

function MyComponent() {

    const callback = useCallback(e => {
        const param = e.target.name
        /* perform action with 'param' */
    }, []);

    return (
        <>
            <Button name='foo' onClick={callback} />
            <Button name='bar' onClick={callback} />
            <Button name='bar' onClick={callback} />
        </>
    );
}

Likewise in complex scenarios, you can pass some sort of id in the child where the child can pass it back in the callback:同样在复杂的场景中,您可以在孩子中传递某种 id,孩子可以在回调中将其传递回:

function Parent() {
    const callback = useCallback((e, param) => {
        /* perform action with 'param' */
    }, []);
    return <>
        <Child id='foo' onChange={callback} />
        <Child id='bar' onChange={callback} />
        <Child id='baz' onChange={callback} />
    </>
}

const Child = memo(({id, onChange}) => {
    return <MyComponent onChange={e => onChange(e, id)} />
})

Although at its face you seem to create a new function in the child, the (memoized) child itself will not re-render on each parent render.尽管从表面上看,您似乎在子级中创建了一个新的 function,但(记忆的)子级本身不会在每个父级渲染上重新渲染。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM