-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
51 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,120 +1,67 @@ | ||
# Chainable Components | ||
_Make your render props composable!_ | ||
_A composable API for reusable React code._ | ||
|
||
From: | ||
Chain together reusable React components: | ||
```jsx | ||
const WithTwoState = props => { | ||
return ( | ||
<WithState initial={0}> | ||
{outer => ( | ||
<WithState initial={outer + 5}> | ||
{inner => ( | ||
props.children({inner, outer}) | ||
)} | ||
</WithState> | ||
) | ||
</WithState> | ||
); | ||
} | ||
``` | ||
To: | ||
```jsx | ||
const withTwoState = | ||
withState({initial: 0}).chain(outer => | ||
withState({initial: outer + 5}).map(inner => | ||
({inner, outer}) | ||
) | ||
withState(0).chain(outer => | ||
withState(outer.value + 5).map(inner => | ||
({inner, outer}) | ||
) | ||
).render(({inner, outer}) => ( | ||
<div> | ||
<div>Outer: {outer.value} <button onClick={() => outer.update(outer.value + 1)}>+</button></div> | ||
<div>Inner: {inner.value} <button onClick={() => inner.update(inner.value + 1)}>+</button></div> | ||
</div> | ||
)); | ||
``` | ||
|
||
### Converting a Render Prop component to a Chainable: | ||
Here's an example of a render prop `WithPromise`: | ||
```jsx | ||
<WithPromise get={() => fetchUser(1234)}> | ||
{user => ( | ||
<div>Hello, {user.username}!</div> | ||
)} | ||
</WithPromise> | ||
``` | ||
You provide a function that returns a promise via the `get` attribute, then it returns the value in that promise via the children callback. In this case, the `fetchUser` function returns a promise that will return a `User`, so a `User` is supplied to the callback. | ||
You can convert this existing Render Prop component to a chainable component with the `fromRenderProp` function: | ||
```jsx | ||
const withPromise = fromRenderProp(WithPromise); | ||
``` | ||
`withState` is now a function that takes a configuration object (which would have been props to the render prop function), and returns a chainable component: | ||
```jsx | ||
const userChainable: ChainableComponent<User> = withPromise({get: () => fetchUser(1234)}); | ||
``` | ||
Transform HOCs and Render Props to chainables and back: | ||
|
||
Since the `get` function will return a promise of `User`, the contextual value inside of this chainable component is `User`, so `userChainable`’s type is `ChainableComponent<User>` (said, “chainable component of user”). | ||
![Chainable pipeline](docsSrc/chainable-pipeline.png?raw=true "Chainable pipeline") | ||
|
||
### Rendering a Chainable Component: | ||
Since the ”wrapped” value’s type is `User`, the `ap` method takes a function which takes a user, and returns the rendered output: | ||
Example: | ||
```jsx | ||
userChainable.render(user => ( | ||
<div>Hello, {user.username}!</div> | ||
import { Route } from 'react-router'; | ||
import { connect } from 'react-redux'; | ||
|
||
const withConnect = fromHigherOrderComponent(connect(mapState, mapDispatch)); | ||
const withRoute = fromRenderProp(Route); | ||
|
||
// withConnect and withRoute are now chainable! | ||
const withConnectAndRoute = | ||
withConnect.chain(storeProps => | ||
withRoute.map(route => ({ | ||
store: storeProps, | ||
path: route.history.location.pathname | ||
}))); | ||
|
||
// then render it! | ||
withConnectAndRoute.render(({store, path}) => ( | ||
<div> | ||
current path is: {path} | ||
store contains: {store.users} | ||
</div> | ||
)); | ||
``` | ||
|
||
All together, this looks like: | ||
```jsx | ||
withPromise({get: () => fetchUser(1234)}) | ||
.render(user => ( | ||
<div>{user.id} - {user.username}</div> | ||
)) | ||
``` | ||
Which is actually quite similar to the render prop version: | ||
```jsx | ||
<WithPromise get={() => fetchUser(1234)}> | ||
{user => ( | ||
<div>{user.id} - {user.username}</div> | ||
// or convert it back render prop: | ||
const ConnectAndRoute = withConnectAndRoute.toRenderProp(); | ||
<ConnectAndRoute> | ||
{({store, path}) => ( | ||
<div> | ||
current path is: {path} | ||
store contains: {store.users} | ||
</div> | ||
)} | ||
</WithPromise> | ||
``` | ||
Why would we go through the trouble of converting a Render Prop to a Chainable Component? The answer is the additional methods a chainable component has, `map` and `chain`. | ||
### Mapping values inside a Chainable Component | ||
Suppose we didn’t care about any information about the user at all, only their role, because we wanted to display something to the user, but only if they were an admin. We could map the user value inside our hoc into a boolean, and then use the boolean when we actually apply the chainable component. For instance, | ||
```jsx | ||
withPromise({get: () => fetchUser(1234)}) | ||
.map(user => user.role === 'Administrator') | ||
.render(isAdmin => ( | ||
isAdmin ? (<div>secret plans...</div>) : | ||
<div>Access denied!</div> | ||
)); | ||
``` | ||
### Composing Chainable Components | ||
Chainable components can be composed, or “chained” easily using the chain method. `chain` is very similar to `map`, except that the function parameter returns another chainable component. This allows you to combine chainables, and use the output of one as input to the other. | ||
<ConnectAndRoute> | ||
|
||
Let’s suppose that you wanted to fetch a user, and when the user loaded, “fade in” the ui so that it looks smooth. You could add the fade in styles to the `WithPromise` render prop, but then it would be applied to all instances where `WithPromise` is used. A better approach, would be to build a separate chainable component, and then compose the two together with `chain`: | ||
// or convert it back to a HOC: | ||
const connectAndRouteHoc = withConnectAndRoute.toHigherOrderComponent(p => p); | ||
|
||
Let's suppose we have a `fadeIn` chainable component, which just applies styles based on configuration passed to it: | ||
```jsx | ||
fadeIn({duration: 500, delay: 0}) | ||
.render(() => ( | ||
<div>This is smooth!</div> | ||
)); | ||
``` | ||
Composing the `withPromise` and `fadeIn` chainable components would look like: | ||
```jsx | ||
const fadeIn = buildChainable(FadeIn); | ||
|
||
withPromise({get: () => fetchUser(1234)}) | ||
.chain(user => | ||
fadeIn({duration: 500, delay: 0}) | ||
.map(() => user)) | ||
.render(user => ( | ||
<div>{user.id} - {user.username}</div> | ||
)) | ||
connectAndRouteHoc(({store, path}) => ( | ||
<div> | ||
current path is: {path} | ||
store contains: {store.users} | ||
</div> | ||
)); | ||
``` | ||
|
||
With the addition of `map` and `chain`, our Render Prop components can now enjoy the composablity of HOC’s, but still keep the declarative nature of Render Props! |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.