Markup as function
If you are writing React applications you probably know about higher order components or render props (which by the way I think is kind of a form of higher order component pattern). In both cases we have a component that encapsulates logic and passes props down to children. Recently at work we came to the idea that we may push this further and represent some functionalities which are out of React in the same fashion - with a single tag in our components tree.
Recently I also saw this on Twitter:
In #React-land: is it legit to have a component that only *does* stuff, but isn't visible? i.e. for setting cookie from a dispatched redux action, or kick off a background task, etc.
— @rem (@rem) November 30, 2017
The idea is interesting so I decided to experiment and see the pros and cons. Imagine how we add/compose functionality with markup only. Instead of doing it in a JavaScript function we just drop a tag. But let’s do couple of examples and see how it looks like.
No matter what we use for our React applications we always have that mapping between logic layer and rendering layer. In the Redux land this is the so called connect
function where we say map this portion of the state to props or map this actions to this props.
function Greeting({ isChristmas }) {
return (
<p>
{ this.props.isChristmas ? 'Merry Christmas' : 'Hello' } dear user!
</p>
);
}
const mapStateToProps = state => ({ isChristmas: state.calendar.isChristmas });
export default connect(mapStateToProps)(Greeting);
isChristmas
is just a boolean for Greeting
. The component doesn’t know where this boolean is coming from. We may easily extract the function into an external file which will make it completely blind for Redux and friends. That is fine and it works well. But what if we have the following:
import IsChristmas from './IsChristmas.jsx';
export default function Greeting() {
return (
<div>
<IsChristmas>
{ answer => answer ? 'Merry Christmas dear user!' : 'Hello dear user!' }
</IsChristmas>
</div>
);
}
Now Greeting
does not accept any properties but still does the same job. It is the IsChristmas
component having the wiring and fetching the knowledge from the state. Then we have the render props pattern to make the decision what string to render.
// IsChristmas.jsx
const IsChristmas = ({ isChristmas, children }) => children(isChristmas);
export default connect(
state => ({ isChristmas: state.calendar.isChristmas })
)(IsChristmas);
Using this technique we are shifting the dependency of the state to an external component. Greeting
becomes a composition layer with zero knowledge of the application state.
This example is a simple one and looks pointless. Let’s go with a more complicated scenario:
function UserProfile() {
return (
<UserDataProvider>{
user => (
<ActionsProvider>{
actions => (
<section>
Hello, { user.fullName },
please <a onClick={ actions.purchase }>order</a> here.
</section>
)
}</ActionsProvider>
)
}</UserDataProvider>
)
}
We have two providers the role of which is to deliver (a) some data for the current user and (b) a redux action creator purchase
so we can fire it when the user click on the order
link. These providers are nothing more then functions that use the children
prop as a regular function:
// UserDataProvider.jsx
function UserDataProvider({ children }) {
return children({ fullName: 'Jon Snow'});
}
connect(state => ({ user: state.user }))
(UserDataProvider);
// ActionsProvider.jsx
function ActionsProvider({ children }) {
return children({ purchase: () => alert('Woo') });
}
connect(null, dispatch => ({ purchase: () => dispatch(purchaseActionCreator()) }))
(UserDataProvider);
This idea shifts the dependencies resolution into JSX syntax which to be honest I really like. We don’t have to know about wiring and on a later stage we may completely swap the provider by just re-implementing the component. For example in the code above if we say that the user’s data comes from the cookie and not from a Redux’s store we may just change the body of UserDataProvider
.
Of course I do see some problems with this approach. First, testing wise we still need the same setup to make our main component testable. UserProfile
still needs the Redux stuff because its internal components are using them. While if we do the wiring directly to UserProfile
we will get user
and purchase
as props and we could mock them. Second, the code looks a little bit ugly if we need to use the render props pattern.
Overall, I don’t know :) The idea seems interesting but as with most of the patterns can not be applied to every case. Let’s see how it evolves and I will post an update soon.