This is a counter-proposal to the current hooks proposal of the React team. The reasons and other questions are answered in the FAQ below (especially Why the factory-pattern if the current react-proposal is so popular?).
You can also find a list of live-demos in the FAQs below.
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>
);
}
- basic usage
- table of contents
- Component-Lifecycle for a factory-component
- basic hooks API reference
- how does
useEffect
works - advanced usage (custom hooks)
- FAQ
- Why the factory-pattern if the current react-proposal is so popular?
- live-examples/working demos?
- what is a factory function and a render function
- TypeScript-typings?
- Why the getter functions overall? #toomuchnoise
- Adding a simple effect without an additional local state to my functional component is too complicated, how can i avoid it?
- history
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.
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.
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.
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.
The argument that const [get..., set...] = useState()
is too much repetition and somewhat cumbersome can hardly be refuted. Some possible alternatives could be:
if you like, please share your opinions and ideas in the related issue
useState
returns one function which is both getter & setter
const state = useState(initialState);
state(); // getter
state(value); // setter
For example, knockoutjs uses this for its observables.
useState
returns an object with get/set
function useState(initialValue) {
// ...
let value; //
// use `v` as shorthand for value, it could be any other syntax
const state = {
get v() {
return value;
},
set v(newValue) {
value = newValue;
// setState to update the react-component
}
};
return state;
}
Which uses JS getter and setter. The usage would then look like:
function Counter() {
const count = useState();
return props => (
<div>
<p>
{props.name} clicked {count.v} times
</p>
<button onClick={() => (count.v = count.v + 1)}>Click me</button>
</div>
);
}
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") during the factory-phase. You can then use your created effect in your render-function:
function ExampleEffect() {
const docTitleEffect = useEffect(name => {
document.title = `Hi ${name}!`;
});
return props => {
docTitleEffect(props.name);
return <div>...</div>;
};
}
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:
useEffect
returns a function (the "effect")- an effect (the returned function of an
useEffect
-call) will not execute itself, but must always be called during a render cycle useEffect
has no possibility to access the currentprops
(see factory function above)- this is per design, because custom hooks have to be independent from the components
- To use variables from
props
, you must pass them to the effect as parameters
- the effect-function can have parameters
- the second parameter must be a function which returns the array
For more information see how does useEffect
works below.
const getContext = useContext(Context);
Like the current useContext
, except it returns a getter-function. See useState
above why we need the getter-function.
Presented functions analogous to the react-docs - additional hooks.
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.
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.)
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>;
}
removed - this can be done with the normal React.createRef
or a normal js-variable (let
):
function Example() {
// to get the ref of of the dom-element, use `React.createRef()`:
const inputEl = React.createRef();
// to get a mutable variable use `let`:
let name;
return props => {
name = props.name;
return <input ref={inputEl} type="text" />;
};
}
// 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.
Prefer the standard
useEffect
when possible to avoid blocking visual updates.
The signature is identical to useEffect
. Otherwise, the behavior is similar to that described in the React documentation.
The basic principle of useEffect
can be described by the following function:
function useEffect(effect) {
let cleanup;
return (...params) => {
if (cleanup) {
cleanup();
}
cleanup = effect(...params);
};
}
If we look at our previous example:
function App() {
const docTitleEffect = useEffect(name => {
document.title = `Hi ${name}!`;
});
return props => {
docTitleEffect(props.name);
return <div>your component</div>;
};
}
When the function is called, the effect function is not executed immediately, but pushed to a kind of ToDo
stack. When the function is finished rendering (like componentDidMount
or componentDidUpdate
for classes), the ToDo stack is processed and all effects called during the render function are executed.
Let's have a look at this component:
function FriendStatus() {
const [getIsOnline, setIsOnline] = useState(null);
const handleStatusChange = status => setIsOnline(status.isOnline);
const friendEffect = useEffect(friendId => {
ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange);
};
});
return props => {
friendEffect(props.friend.id);
if (getIsOnline() === null) {
return "loading...";
}
return getIsOnline() ? "Online" : "Offline";
};
}
The created friendEffect
is called on every render-cycle and so we could subscribe and unsubscribe to the same friend again and again. But there are two possibilities to skip effects:
by the 2nd parameter of useEffect
:
function FriendStatus() {
const [getIsOnline, setIsOnline] = useState(null);
const handleStatusChange = status => setIsOnline(status.isOnline);
const friendEffect = useEffect(
friendId => {
/* ... (same effect-function as above)*/
},
// this is a function which gets the same parameters like your effect-function and which must return an array
// your effect-function will now only called when one of the array-items will change
friendId => [friendId]
);
return props => {
friendEffect(props.friend.id);
// return your ui
};
}
Or you can perform the effect conditionally in the render function:
function FriendStatus() {
const [getIsOnline, setIsOnline] = useState(null);
const handleStatusChange = status => setIsOnline(status.isOnline);
const friendEffect = useEffect(friendId => {
/* ... (same effect-function as above)*/
});
return props => {
if (props.subscribeToFriendStatus) {
friendEffect(props.friend.id);
}
// return your ui
};
}
This form makes it easy to create customized versions of useEffect
. For example, a version could be useMemoizedEffect
, which could implement the skipping-effect example from above accordingly:
function useFriendStatus() {
const [getIsOnline, setIsOnline] = useState(null);
const handleStatusChange = status => setIsOnline(status.isOnline);
// 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-implementation 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);
};
}
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.
The selected example corresponds to the one in the React documentation.
function useFriendStatus() {
const [getIsOnline, setIsOnline] = useState(null);
const 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";
};
}
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).
I published a basic implementation under this package (yarn add react-factory-hooks
), where I implemented useEffect
and useState
via a factory
-HOC.
Here are some live-demos via codesandbox, using this package:
- Basic example (Counter)
- Counter example with an effect
- example of an effect/state via a factory and a
UseEffect
-component - custom hook example
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>;
I added a index.d.ts
-type-file in this repo. Especially type-hints for effects works quite good:
function useFriendStatus() {
const [getIsOnline, setIsOnline] = useState(null);
const handleStatusChange = status => setIsOnline(status.isOnline);
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'.
// ...
};
}
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.
Adding a simple effect without an additional local state to my functional component is too complicated, how can i avoid it?
You can create a normal react-component for this use-case. 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>
</>
);
}
This form gives you a great flexibility in the implementation of UseEffect
, e.g.:
// the effect as children:
<UseEffect>
{() => { /* ... */ }}
</UseEffect>
// current behavior of reacts `useEffect`:
<UseEffect
whenItemsDidChange={[props.name]}
effect={() => { /* ... */ }}
/>
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.
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.
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.