rfh
react-factory-hooks
npm i react-factory-hooks
rfh

react-factory-hooks

Implementing react hooks via the factory-pattern

by PutziSan

0.3.1 (see all)License:MITTypeScript:Not Found
npm i react-factory-hooks
Readme

basic usage

A normal "stateful" functional Component looks like this:

function Counter() {
  const [getCount, setCount] = useState();

  return props => (
    <div>
      <p>
        {props.name} clicked {getCount()} times
      </p>
      <button onClick={() => setCount(getCount() + 1)}>Click me</button>
    </div>
  );
}

table of contents

Component-Lifecycle for a factory-component

The Component-Lifecycle with a factory-function changes a little bit. Before rendering a component for the first time, the factory function is executed once (similar to the constructor in a class component), which returns the render function. For subsequent rendering, only the render function is executed again.

This has the advantage that the use* functions do not have to be executed again and again with each rendering.

Let's imagine we have this factory-component:

function SimpleCounter() {
  const [getCount, setCount] = useState();

  return props => (
    <button onClick={() => setCount(getCount() + 1)}>{getCount()}</button>
  );
}

When using this component (e.g. ReactDOM.render(<SimpleCounter />, root);), const [getCount, setCount] = useState(); is executed only one single time. If you click on the rendered button, only the render function is called again and the button is re-rendered.

basic hooks API reference

The API for basic use is described below. The presented functions are based on the documentation of the official React page (Hooks API Reference) and additionally the signature for the factory function..

"basic use" refers to common use in developing React components. This covers adding a local state to a functional component. To create hooks that can be reused and e.g. use "effects", see advanced usage (custom hooks) below.

factory function

function Factory(initialProps) {
  // the use*-functions can only be used here

  // must return a normal react-function-component
  // the component can use the local states and variables defined in the factory
  return props => <div />;
}

See Component-Lifecycle for a factory-component for information how the factory-function works.

With the initialProps parameter, a local state can be initialized with a certain value:

function Example(initialProps) {
  // getCount will have initialProps.startCount as default-value
  const [getCount, setCount] = useState(initialProps.startCount);

  return props => (
    <div>{getCount()} (will be startCount, until you call `setCount`)</div>
  );
}

Be careful, initialProps will not change! It will always point to the the props-object with which it was called the first time it was rendered.

If you want to access the current props in an "effect" (as it is possible in the current proposal by the react-team), see But how can I realize side effects in my functional components? below.

useState

const [getState, setState] = useState(initialState);

differences to current react-proposal:

  • returns a getter- and a setter-function for the local state. The getter-function always returns the current state-value

Since the factory function is not repeated every time, the first element of the returned array must be a getter function so that the render function can still access the current state.

The rest is congruent with the current proposal, see React-Docs - useState-API.

This function is not recommended for basic use.

In a later documentation useEffect should no longer appear under "basic API reference". It is currently only listed here to follow the React documentation.

useContext

const getContext = useContext(Context);

Like the current useContext, except it returns a getter-function. See useState above why we need the getter-function.

additional hooks

Presented functions analogous to the react-docs - additional hooks.

useReducer

const [getState, dispatch] = useReducer(reducer, initialState);

Like the current useReducer, except the first element of the returned array is a getter-function. See useState above why we need the getter-function.

In my opinion, I wouldn't add this to the core as it can be built with setState and could encourage people to recommend that state-handling with a reducer (and inevitably associated redux) is recommended by the React team. But, as I said, that's just IMO.

useCallback

removed - this can be easily done with pure JS with this proposal. basic example:

function Example() {
  const handleClick = e => {
    console.log("clicked");
  };

  return props => <button onClick={handleClick}>click me</button>;
}

For an advanced example, where the callback changes when on of its input changes, we can use a memoization-library like memoize-one:

import memoizeOne from "memoize-one";

function Example() {
  const handleClick = memoizeOne((a, b) => e => {
    console.log(`clicked with ${a} and ${b}`);
  });

  return props => (
    <button onClick={handleClick(props.a, props.b)}>click me</button>
  );
}

(Note that the React team has so far recommended this procedure with a memoization-library, so that is nothing new in the react-eco-system.)

useMemo

removed - this can be done with a normal memoization-library like memoize-one:

import memoizeOne from "memoize-one";

function Example() {
  const expensiveComputation = memoizeOne((a, b) => {
    // ... your memory-intensive-function
    return "your-computed-value";
  });

  return props => <p>{expensiveComputation(props.a, props.b)}</p>;
}

useRef

removed - this can be done with the normal React.createRef:

function Example() {
  const inputEl = React.createRef();
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };

  return props => (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useImperativeMethods

// TODO

To be honest, I don't fully understand the meaning of this hook and I have no idea how it works internally. Maybe it can also be replaced with a normal JS construct. To be honest, I don't fully understand the meaning of this hook and I have no idea how it works internally. Maybe it can also be replaced with a normal JS construct. Otherwise it could be taken over exactly as currently described in the documentation.

These functions are not recommended for basic use.

In a later documentation useMutationEffect and useLayoutEffect should no longer appear under "basic API reference". It is currently only listed here to follow the React documentation.

advanced usage (custom hooks)

One of the biggest strengths of hooks is that you can easily build your own hooks to build logic independent of components (see react-documentation: "Building Your Own Hooks").

This proposal also makes this possible. An example of this can be seen below.

To be able to access "effects" (lifecycle-events) independently from a component, the useEffect API is additionally provided.

custom hook example

The selected example corresponds to the one in the React documentation.

function useFriendStatus() {
  const [getIsOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  const friendEffect = useEffect(friendId => {
    ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange);
    };
  });

  return friendId => {
    friendEffect(friendId);
    return getIsOnline();
  };
}

and the usage of this custom hook:

function FriendStatus() {
  const getIsFriendOnline = useFriendStatus();

  return props => {
    const isOnline = getIsFriendOnline(props.friend.id);

    if (isOnline === null) {
      return "loading...";
    }
    return isOnline ? "Online" : "Offline";
  };
}

useEffect

useEffect should only be used in your custom hooks, if you just want to do side-effects on changes (mount or updated props), please see But how can I realize side effects in my functional components? below.

const effect = useEffect(
  (...params) => {
    /* your effect-function */

    // the effect-function can optionally return a cleanup-handler
    return () => {
      /* ... cleanup */
    };
  },
  /* optional: */ (...params) => [
    /* ... conditionally firing an effect */
  ]
);

useEffect returns an executable function (the "effect"). Whenever the effect is called in a render cycle, the effect function is executed with the parameters passed to the effect (but only after rendering is finished). If the component is unmounted, the last cleanup handler (if specified) is executed.

differences to current react-proposal:

  • an effect will not execute itself, but must always be called during a render cycle
  • useEffect has no possibility to access the current props (see factory function above)
    • this is per design, because custom hooks have to be independent from the components
    • if you want to access props in an effect, see the FAQ below
  • the effect-function can have parameters
  • the second parameter must be a function which returns the array

possibilities to customize useEffect.

This form makes it easy to create customized versions of useEffect. For example, a version could be useMemoizedEffect, which could implement the skipping-effects-example accordingly::

function useFriendStatus() {
  // ...

  // the following is equal to useEffect(friendId => { ... }, (friendId) => [friendId]);
  const friendEffect = useMemoizedEffect(friendId => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  return friendId => {
    friendEffect(friendId); // friendEffect will now only fire if friendId changed
    return getIsOnline();
  };
}

Where the implementation of useMemoizedEffect might look like this:

function useMemoizedEffect(effectFn) {
  const effect = useEffect(effectFn);

  let lastParams;
  return (...params) => {
    // arrayElementsEqu-implentation omitted
    // (it checks whether the elements in both arrays are the same for each index (using `===`))
    if (arrayElementsEqu(params, lastParams)) {
      return;
    }

    lastParams = params;

    return effect(...params);
  };
}

faq

Answers to a few questions that I can imagine will come up more often.

The current proposal from the React team has not only been received positively and cheeringly. The famous rfc-github-issue#68 has over 1000 comments with questions, counterproposals and uncertainties.

The idea is really good, but the implementation within the render function unsettles many. The "magic" of the functions is cited as a negative point, since they can tell by themselves whether they need to reinitialize a state, or whether this component has already been initialized and thus return the state of the component. Furthermore, the "rules of hooks" are strongly criticized because they are not intuitive and can only be forced with a linter.

I believe that although the idea is very popular, the current implementation proposal is not optimal and I have therefore drafted this counter-proposal.

The "magic" can be better explained by this suggestion (see Component-Lifecycle for a factory-component above) and the "rules of hooks" apply to this proposal as well, but they are now intuitively forced by the language of JavaScript (by variable scopes) and work as expected.

And finally, my suggestion can make the API leaner, since many current use* functions can be mapped by normal JavaScript features and so no new API has to be learned (see additional hooks above).

But how can I realize side effects in my functional components?

Use a normal react-Component. A basic implementation could look like this:

class UseEffect extends React.Component {
  componentDidMount() {
    this.cleanup = this.props.effect();
  }
  componentDidUpdate() {
    if (this.cleanup) {
      this.cleanup();
    }
    this.cleanup = this.props.effect();
  }
  componentWillUnmount() {
    if (this.cleanup) {
      this.cleanup();
    }
  }
  render() {
    return null;
  }
}

and the usage:

function App(props) {
  return (
    <>
      <UseEffect
        effect={() => {
          document.title = `Hi ${props.name}!`;
          return () => {
            console.log("cleanup");
          };
        }}
      />
      <div>your component</div>
    </>
  );
}

A corresponding component could be included in the react-core, but you could/should leave this to the community (and new packages) in which form they want to handle their normal side effects.

This form gives you a great flexibility, e.g.:

// the effect as children:
<UseEffect>
  {() => { /* ... */ }}
</UseEffect>
// current behavior of reacts `useEffect`:
<UseEffect
  whenItemsDidChange={[props.name]}
  effect={() => { /* ... */ }}
/>

If you do not want to handle side-effects inside JSX, you could of course use the here proposed useEffect:

function App() {
  const docTitleEffect = useEffect(name => {
    document.title = `Hi ${name}!`;
  });

  return props => {
    docTitleEffect(props.name);

    return <div>your component</div>;
  };
}

what is a factory function and a render function

The "factory pattern" works with an outer function, which is executed once for "initialization" before the first render, and an inner function, which reflects the visual react component. In the following I will work with the terms "factory function" (outer wrapping function) and "render function" (inner function that provides the React component).

In the first example above is the factory-function:

function Counter() {
  const [getCount, setCount] = useState();

  return ... // returns the-render-function
}

In the first example above is the render function:

return props => <div>...</div>;

TypeScript-typings?

I added a index.d.ts-type-file in this repo. Especially type-hints for effects works quite good:

function useFriendStatus() {
  // ...

  const friendEffect = useEffect((friendId: string) => {
    // ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
    return () => {
      // ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange);
    };
  });

  return (friendId: number) => {
    friendEffect(friendId); // TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
    // ...
  };
}

Why the getter functions overall? #toomuchnoise

See useState to understand why they need to be getter functions.

About the noise: it's true, through the getter functions there are 2 (brackets for function call) + 3 (if you put a get before the variable name) = 5 characters more. However, I believe that this is reasonable, as opposed to the advantages that are gained. Especially if you use TypeScript, you can avoid the get at the front, because the typing makes it clear that it is a function.

history

This is the 3rd draft of the proposal to implement hooks using a factory pattern. I will briefly discuss the previous drafts and describe the insights.

first draft - props overall

function FactoryExample(initialProps) {
  const [getCount, setCount] = useState(initialProps.startCount);

  useEffect(props => {
    document.title = `Hi ${props.name}! You clicked ${getCount()} times`;
  });

  return props => (
    <div>
      <p>
        {props.name} clicked {getCount()} times
      </p>
      <button onClick={() => setCount(getCount() + 1)}>Click me</button>
    </div>
  );
}

see this version of the proposal here (tag v0.1.0)

This is theoretically a very clean idea, but it makes a lot of noise, because props has to appear as a parameter in every effect (which is not nice, but would be acceptable for a clearer API). However, the bigger drawback is that effects are no longer independent of the components and can quickly be misused, resulting in worse code. Thx @mcjazzyfunky and @FredyC for pointing this out in the issue.

second draft - getProps in the wrapping function

function FactoryExample(getProps) {
  const [getCount, setCount] = useState(getProps().startCount);

  useEffect(() => {
    document.title = `Hi ${getProps().name}! You clicked ${getCount()} times`;
  });

  return props => (
    <div>
      <p>
        {props.name} clicked {getCount()} times
      </p>
      <button onClick={() => setCount(getCount() + 1)}>Click me</button>
    </div>
  );
}

see this version of the proposal here (tag v0.2.0)

This idea originally came from @zouloux for preact, which he also published under solid-js/prehook-proof-of-concept. Since it solved the problem of the first draft, I built it into my second draft with a small adjustment (props should be included in the render function as a parameter).

One disadvantage was that it just didn't feel good to have props and getProps duplicated and also the access in effects via getProps wasn't very intuitive. A much bigger disadvantage was described by @FredyC in the Comments on @zouloux suggestion:

I think you are both forgetting one important detail ... default props and I don't mean that hack-ish static defaultProps property, but real default values you can define when destructuring props in a component scope. It means much better colocation and it's clear what default value is for a particular prop.

Downloads/wk

0

GitHub Stars

24

LAST COMMIT

4yrs ago

MAINTAINERS

1

CONTRIBUTORS

1

OPEN ISSUES

2

OPEN PRs

0
VersionTagPublished
0.3.1
latest
4yrs ago
No alternatives found
No tutorials found
Add a tutorial

Rate & Review

100
No reviews found
Be the first to rate