RFC: Hooks
diegohaz opened this issue · 1 comments
React hooks were announced this week and will be available in React 16.7. I'm opening this issue to talk about an addition (or maybe a replacement) on this library's API.
Purpose of this library
First, it's important to clarify the purpose of this library. A big part of the state of our applications should be local, and some should be shared, contextual or global (whatever people call it). But we don't always know whether a state should be local or shared when first writing it.
Having to refactor local state into Context or Redux later could be cumbersome. Many choose to write global state (using Redux, MobX, React Context etc.) upfront just because some day they may need it.
Constate makes it easier to write local state and make it global later.
And that's why it's called Constate. Context + State.
UPDATE: Final API
Provider
Provides state to its children;
import { Provider } from "constate";
function App() {
return (
<Provider devtools>
<Counter />
</Provider>
);
}
useContextState
Same as React's useState
, but accepting a context key argument so it accesses the shared state on Provider
.
import { useContextState } from "constate";
function Counter() {
const [count, setCount] = useContextState("counter1", 0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
useContextReducer
will be available as well.
useContextEffect
Effect that runs on Provider
so it will run only once per context key, not per consumer.
import { useContextState, useContextEffect } from "constate";
function Counter() {
const [count, setCount] = useContextState("counter1", 0);
useContextEffect("counter1", () => {
document.title = count;
}, [count]);
return <div>{count}</div>;
}
Other methods such as useContextMutationEffect
and useContextLayoutEffect
will be available as well.
Context
The Context
object will be exposed so users will be able to useContext
directly to access the whole shared state tree:
import { useContext } from "react";
import { Context } from "constate";
function Counter() {
const [state, setState] = useContext(Context);
const incrementCounter1 = () => setState({
...state,
counter1: state.counter1 + 1
});
return <button onClick={incrementCounter1}>{state.counter1}</button>;
}
createContext
Alternatively, a new context object can be created:
// context.js
import { createContext } from "constate";
const {
Context,
Provider,
useContextState,
useContextEffect
} = createContext({ counter1: 0 });
export {
Context,
Provider,
useContextState,
useContextEffect
};
import { useContextState } from "./context";
function Counter() {
const [count, setCount] = useContextState("counter1");
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Alternative 1: useState
/ useEffect
Given the following example using normal React useState
:
import { useState } from "react";
const CounterButton = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return <button onClick={increment}>{count}</button>;
};
Say we want to share that count state between multiple components. My idea is to export an alternative useState
method so people can pass an additional context
argument to access a shared state.
import React from "react";
// import `useState` from constate instead of react
import { Provider, useState } from "constate";
const CounterButton = () => {
// the only difference here is the additional argument
const [count, setCount] = useState(0, "counter1");
const increment = () => setCount(count + 1);
return <button onClick={increment}>{count}</button>;
};
// Another component that accesses the same shared state
const CounterText = () => {
const [count] = useState(0, "counter1");
return <p>You clicked {count} times</p>;
};
const App = () => (
<Provider devtools>
<CounterText />
<CounterButton />
</Provider>
);
Also, it could be abstracted so as to provide something similar to what we have today:
// useCounter.js
import { useState } from "constate";
function useCounter({ initialState, context } = {}) {
const [state, setState] = useState({ count: 0, ...initialState }, context);
const increment = () => setState(prevState => ({
...prevState,
count: prevState.count + 1
});
return { ...state, increment };
}
export default useCounter;
// Counter.js
import React from "react";
import useCounter from "./useCounter";
const Counter = () => {
const { count, increment } = useCounter({ context: "counter1" });
return (
<div>
<p>You clicked {count} times</p>
<button onClick={increment}>Click me</button>
</div>
);
}
Currently, we have some lifecycle props such as onMount
, onUpdate
and onUnmount
. They serve not only as a nicer way to use lifecycles within functional components, but they also guarantee that they will trigger per context
(if the prop is passed), and not per component.
Now we can use lifecycles within functions with useEffect
, so Constate doesn't have to intrude on it anymore. But the second problem still persists.
As an example, consider a shared state that would fetch some data from the server in componentDidMount. If the state is shared, this should be triggered only once (when the first component gets mounted, for example), and not once per component that accesses that state.
I'm still not sure how to make it work. Maybe we would need to export an alternative useEffect
to do the job.
Alternative 2: useConstate
/useConstateEffect
Instead of useState
and useEffect
, we can just export useConstate
and useConstateEffect
:
import { useConstate, useConstateEffect } from "constate";
const Counter = () => {
const context = "counter1";
const [count, setCount] = useConstate({ initialState: 0, context });
useConstateEffect({
create: () => {
document.title = count;
},
inputs: [count],
context
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={increment}>Click me</button>
</div>
);
};
That's because the signatures can't be the same. When using context
, most of the time we won't set initialState
. Having it as the first argument would require us to write useState(undefined, "context")
all the time, which can be cumbersome.
An object parameter solves this problem. The keys follow the parameter names of the original methods on React codebase.
Alternative 3: useConstate
import { useConstate } from "constate";
const Counter = () => {
const { useState, useEffect } = useConstate("counter1");
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
useEffect(() => {
document.title = count;
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={increment}>Click me</button>
</div>
);
};
- import { useState, useEffect } from "react";
+ import { useConstate } from "constate";
const Counter = () => {
+ const { useState, useEffect } = useConstate("counter1");
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
useEffect(() => {
document.title = count;
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={increment}>Click me</button>
</div>
);
};
Alternative 4: useContextState
/useContextEffect
import { useContextState, useContextEffect } from "constate";
const Counter = () => {
const [count, setCount] = useContextState("counter1", 0);
const increment = () => setCount(count + 1);
useContextEffect(
"counter1",
() => {
document.title = count;
},
[count]
);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={increment}>Click me</button>
</div>
);
};
This looks brilliant.