Skip to content

Commit

Permalink
feat: Return a hook from the createContainer method (#78)
Browse files Browse the repository at this point in the history
Closes #77
  • Loading branch information
timkindberg authored and diegohaz committed Apr 14, 2019
1 parent 1889be8 commit 8de6eb6
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 84 deletions.
73 changes: 39 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,39 +36,39 @@ Write local state using [React Hooks](https://reactjs.org/docs/hooks-intro.html)

```jsx
import React, { useState, useContext } from "react";
import createContainer from "constate";
import createUseContext from "constate";

// 1️⃣ Create a custom hook as usual
function useCounter() {
const useCounter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(prevCount => prevCount + 1);
return { count, increment };
}

// 2️⃣ Create container
const CounterContainer = createContainer(useCounter);
// 2️⃣ When you need to share your state, simply wrap your hook
// with the createUseContext higher-order hook, like so:
const useCounterContext = createUseContext(useCounter);

function Button() {
// 3️⃣ Use container context instead of custom hook
// const { increment } = useCounter();
const { increment } = useContext(CounterContainer.Context);
// 3️⃣ Use the new container hook to extract the value from the wrapped hook.
const { increment } = useCounterContext()
return <button onClick={increment}>+</button>;
}

function Count() {
// 4️⃣ Use container context in other components
// const { count } = useCounter();
const { count } = useContext(CounterContainer.Context);
// 4️⃣ But now you can use it in other components as well.
const { count } = useCounterContext()
return <span>{count}</span>;
}

function App() {
// 5️⃣ Wrap your components with container provider
// 5️⃣ The caveat: you wrap your components with the Provider that is
// attached to the container hook
return (
<CounterContainer.Provider>
<useCounterContext.Provider>
<Count />
<Button />
</CounterContainer.Provider>
</useCounterContext.Provider>
);
}
```
Expand All @@ -89,50 +89,54 @@ yarn add constate

## API

### `createContainer(useValue[, createMemoInputs])`
### `createUseContext(useValue[, createMemoInputs])`

Constate exports a single method called `createContainer`. It receives two arguments: [`useValue`](#usevalue) and [`createMemoInputs`](#creatememoinputs) (optional). And returns `{ Context, Provider }`.
Constate exports a single higher-order hook called `createUseContext`. It receives two arguments: [`useValue`](#usevalue)
and [`createMemoInputs`](#creatememoinputs) (optional). And returns a wrapped hook that can now read state from the
Context. The hook also has two static properties: `Provider` and `Context`.

#### `useValue`

It's a [custom hook](https://reactjs.org/docs/hooks-custom.html) that returns the Context value:
It's any [custom hook](https://reactjs.org/docs/hooks-custom.html) that returns a value:

```js
import React, { useState } from "react";
import createContainer from "constate";
import { useState } from "react";
import createUseContext from "constate";

const CounterContainer = createContainer(() => {
const useCounterContext = createUseContext(() => {
const [count] = useState(0);
return count;
});

console.log(CounterContainer); // { Context, Provider }
console.log(useCounterContext); // React Hook
console.log(useCounterContext.Provider); // React Provider
console.log(useCounterContext.Context); // React Context (if needed)
```

You can receive arguments in the custom hook function. They will be populated with `<Provider />`:

```jsx
const CounterContainer = createContainer(({ initialCount = 0 }) => {
const useCounterContext = createUseContext(({ initialCount = 0 }) => {
const [count] = useState(initialCount);
return count;
});

function App() {
return (
<CounterContainer.Provider initialCount={10}>
<useCounterContext.Provider initialCount={10}>
...
</CounterContainer.Provider>
</useCounterContext.Provider>
);
}
```

The value returned in `useValue` will be accessible when using `useContext(CounterContainer.Context)`:
The API of the containerized hook returns the same value(s) as the original, as long as it is a descendant of the Provider:

```jsx
import React, { useContext } from "react";

function Counter() {
const count = useContext(CounterContainer.Context);
const count = useCounterContext();
console.log(count); // 10
}
```
Expand All @@ -144,14 +148,15 @@ Optionally, you can pass in a function that receives the `value` returned by `us
If `createMemoInputs` is undefined, it'll be re-evaluated everytime `Provider` renders:

```js
// re-render consumers only when value.count changes
const CounterContainer = createContainer(useCounter, value => [value.count]);

function useCounter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return { count, increment };
}
const useCounterContext = createUseContext(
() => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return { count, increment };
},
// re-render consumers only when value.count changes
value => [value.count]
);
```

This works similarly to the `inputs` parameter in [`React.useEffect`](https://reactjs.org/docs/hooks-reference.html#useeffect) and other React built-in hooks. In fact, Constate passes it to [`React.useMemo`](https://reactjs.org/docs/hooks-reference.html#usememo) `inputs` internally.
Expand All @@ -161,7 +166,7 @@ You can also achieve the same behavior within the custom hook. This is an equiva
```js
import { useMemo } from "react";

const CounterContainer = createContainer(() => {
const useCounterContext = createUseContext(() => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
// same as passing `value => [value.count]` to `createMemoInputs` parameter
Expand Down
14 changes: 7 additions & 7 deletions examples/counter/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useContext } from "react";
import createContainer from "constate";
import React, { useState } from "react";
import createUseContext from "constate";

// 1️⃣ Create a custom hook as usual
function useCounter() {
Expand All @@ -9,27 +9,27 @@ function useCounter() {
}

// 2️⃣ Create container
const CounterContainer = createContainer(useCounter, value => [value.count]);
const useCounterContext = createUseContext(useCounter, value => [value.count]);

function Button() {
// 3️⃣ Use container context instead of custom hook
const { increment } = useContext(CounterContainer.Context);
const { increment } = useCounterContext();
return <button onClick={increment}>+</button>;
}

function Count() {
// 4️⃣ Use container context in other components
const { count } = useContext(CounterContainer.Context);
const { count } = useCounterContext();
return <span>{count}</span>;
}

function App() {
// 5️⃣ Wrap your components with container provider
return (
<CounterContainer.Provider>
<useCounterContext.Provider>
<Count />
<Button />
</CounterContainer.Provider>
</useCounterContext.Provider>
);
}

Expand Down
14 changes: 7 additions & 7 deletions examples/i18n/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useContext } from "react";
import createContainer from "constate";
import React, { useState } from "react";
import createUseContext from "constate";

const translations = {
en: {
Expand All @@ -17,15 +17,15 @@ function useI18n() {
}

// Only re-evaluate useI18n return value when lang changes
const I18n = createContainer(useI18n, value => [value.lang]);
const useI18NContext = createUseContext(useI18n, value => [value.lang]);

function useTranslation(key) {
const { lang } = useContext(I18n.Context);
const { lang } = useI18NContext();
return translations[lang][key];
}

function Select(props) {
const { lang, locales, setLang } = useContext(I18n.Context);
const { lang, locales, setLang } = useI18NContext();
return (
<select {...props} onChange={e => setLang(e.target.value)} value={lang}>
{locales.map(locale => (
Expand All @@ -42,11 +42,11 @@ function Label(props) {

function App() {
return (
<I18n.Provider>
<useI18NContext.Provider>
<Label htmlFor="select" />
<br />
<Select id="select" />
</I18n.Provider>
</useI18NContext.Provider>
);
}

Expand Down
24 changes: 12 additions & 12 deletions examples/theming/App.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React, { useState, useContext } from "react";
import createContainer from "constate";
import React, { useState } from "react";
import createUseContext from "constate";
import { GithubPicker } from "react-color";

const Theme = createContainer(props => useState(props.initialColor));
const PickerVisibility = createContainer(() => useState(false));
const useThemeContext = createUseContext(props => useState(props.initialColor));
const usePickerVisibilityContext = createUseContext(() => useState(false));

function Picker() {
const [color, setColor] = useContext(Theme.Context);
const [visible, setVisible] = useContext(PickerVisibility.Context);
const [color, setColor] = useThemeContext();
const [visible, setVisible] = usePickerVisibilityContext();
return visible ? (
<GithubPicker
style={{ position: "absolute" }}
Expand All @@ -22,8 +22,8 @@ function Picker() {
}

function Button() {
const [background] = useContext(Theme.Context);
const [visible, setVisible] = useContext(PickerVisibility.Context);
const [background] = useThemeContext();
const [visible, setVisible] = usePickerVisibilityContext();
return (
<button style={{ background }} onClick={() => setVisible(!visible)}>
Select color: {background}
Expand All @@ -33,12 +33,12 @@ function Button() {

function App() {
return (
<Theme.Provider initialColor="red">
<PickerVisibility.Provider>
<useThemeContext.Provider initialColor="red">
<usePickerVisibilityContext.Provider>
<Button />
<Picker />
</PickerVisibility.Provider>
</Theme.Provider>
</usePickerVisibilityContext.Provider>
</useThemeContext.Provider>
);
}

Expand Down
12 changes: 6 additions & 6 deletions examples/typescript/App.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
// It just works! No need to type anything explicitly.
import * as React from "react";
import createContainer from "constate";
import createUseContext from "constate";

function useCounter({ initialCount = 0 } = {}) {
const [count, setCount] = React.useState(initialCount);
const increment = () => setCount(count + 1);
return { count, increment };
}

const CounterContainer = createContainer(useCounter, value => [value.count]);
const useCounterContext = createUseContext(useCounter, value => [value.count]);

function IncrementButton() {
const { increment } = React.useContext(CounterContainer.Context);
const { increment } = useCounterContext();
return <button onClick={increment}>+</button>;
}

function Count() {
const { count } = React.useContext(CounterContainer.Context);
const { count } = useCounterContext();
return <span>{count}</span>;
}

function App() {
return (
<CounterContainer.Provider initialCount={10}>
<useCounterContext.Provider initialCount={10}>
<Count />
<IncrementButton />
</CounterContainer.Provider>
</useCounterContext.Provider>
);
}

Expand Down
24 changes: 12 additions & 12 deletions examples/wizard-form/App.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useContext, useState, useEffect } from "react";
import createContainer from "constate";
import React, { useState, useEffect } from "react";
import createUseContext from "constate";

const Step = createContainer(useStep, value => [value.step]);
const Form = createContainer(useFormState, value => [value.values]);
const useStepContext = createUseContext(useStep, value => [value.step]);
const useFormContext = createUseContext(useFormState, value => [value.values]);

function useStep({ initialStep = 0 } = {}) {
const [step, setStep] = useState(initialStep);
Expand Down Expand Up @@ -35,7 +35,7 @@ function useFormInput({ register, values, update, name, initialValue = "" }) {
}

function AgeForm({ onSubmit }) {
const state = useContext(Form.Context);
const state = useFormContext();
const age = useFormInput({ name: "age", ...state });
return (
<form
Expand All @@ -51,7 +51,7 @@ function AgeForm({ onSubmit }) {
}

function NameEmailForm({ onSubmit, onBack }) {
const state = useContext(Form.Context);
const state = useFormContext();
const name = useFormInput({ name: "name", ...state });
const email = useFormInput({ name: "email", ...state });
return (
Expand All @@ -72,12 +72,12 @@ function NameEmailForm({ onSubmit, onBack }) {
}

function Values() {
const { values } = useContext(Form.Context);
const { values } = useFormContext();
return <pre>{JSON.stringify(values, null, 2)}</pre>;
}

function Wizard() {
const { step, next, previous } = useContext(Step.Context);
const { step, next, previous } = useStepContext();
const steps = [AgeForm, NameEmailForm];
const isLastStep = step === steps.length - 1;
const props = {
Expand All @@ -91,12 +91,12 @@ function Wizard() {

function App() {
return (
<Step.Provider>
<Form.Provider initialValues={{ age: 18 }}>
<useStepContext.Provider>
<useFormContext.Provider initialValues={{ age: 18 }}>
<Wizard />
<Values />
</Form.Provider>
</Step.Provider>
</useFormContext.Provider>
</useStepContext.Provider>
);
}

Expand Down

0 comments on commit 8de6eb6

Please sign in to comment.