This article is split into two parts.
The first part tries to explain at a general level dependency injection in
React applications, describes a possible way to implement it and how to it is
possible to interact with Redux and React Router.
The second part goes a bit further and shows how to implement simple
time travelling with both Redux and React Router.
Dependencies… what’s the problem?
As many others, I like how React makes things really easy.
You can quickly build presentational components, generalize them, and
then add decorations.
The problems usually start when you are in the left-alone land, where you need
to interact with some object completely unrelated to any view. It can be a
service to fetch the data, a clock to synchronize all the components, or even
some wrapper around the local storage.
What to do in this case? Nothing is “explicitly” mentioned in the
documentation as React comes with the flexibility to do whatever you want, so
how I am supposed to deal with those services which are external to my
components?
It turns out, the documentation does not mention anything “explicitly”, but “implicitly” tells a lot. There are several approaches and I hope to describe one of them in an understandable way.
In this article, we are not going to order tacos or wait for some pizza. We are going to have a look at a real world example, a piece of left-alone land: validating a login form.
Here is a possible implementation of the form and the inputs following the official recommendations
class Login extends React.Component {
constructor(props) {
super(props);
this.state = { name: '', password: '', error: null }
}
render() {
const { name, password, error } = this.state;
return (
<form className="Login"
onSubmit={(e) => {
e.preventDefault();
// login: not defined yet !
login(name, password);
}}>
<input type="text" name="name" value={ name }
onChange={e => {
const value = e.target.value;
this.setState({ name: value });
}} />
<input type="password" name="password" value={ password }
onChange={e => {
const value = e.target.value;
this.setState({ password: value });
}} />
<input type="submit" value="Login" />
</form>
);
}
};
We are going to focus only on the login method and a few possible ways to implement it.
But first, we need to decide what the login has to do. Let’s suppose the login functionality should:
- send a request to some backend with the credentials to create a token
- when receiving the token store it into the local storage
- redirect to the last visited page
This is quite a lot for a simple action !
And it already needs 3 dependencies:
- a backend service to send requests to and receive responses from
- the local storage
- a client-side history to redirect
Here is a first naive implementation of the login function
/// login.js
// our custom backend service
import fetchCreate from 'path/to/backend';
// our custom localStorage service wraps getItem and setItem to
// handle json formats automatically and add prefixes to the properties
import rememberToken from 'path/to/localStorage';
// leveraging some external library to work with the history API
import history from 'path/to/history';
async login(name, password) {
const { token } = await fetchCreate(
'authentication',
{ name, password }
);
rememberToken(token);
history.goToEntry();
};
/// Login.jsx
import login from path/to/login/action
class Login {
// ...
}
At first glance, this code looks absolutely fine. But it has a flaw: the import statement. And for at least 3 reasons.
First of all, testing.
Importing requires stubbing functions. Sure it can be done in a dynamical
language, but personally I prefer to avoid it if I can. It slows down my
productivity a lot and I need to write tests that first fail to make sure they
eventually pass.
You may think I need to improve my testing skills and you are probably right,
or perhaps I am too lazy, but how much time have I lost because of small
mistakes that lead me to think I am testing the right function while
in fact I am not? And most importantly how confident can I be about my
tests?
Second, and perhaps more important, it does not look like the React way.
The login function is not a dependency of the Login component. It is a proxy
handler of the login functionality.
React instead asks you to have components that, regardless the fact of having
a lifecycle or not, are the result of a transformation applied on the props
passed to the component.
For stateless component it is quite easy to imagine as they are just pure
render functions.
For stateful components, the lifecycle hooks allowing interaction with the
outside world receive new props as parameters and are treated as functions
too.
Third, this approach makes too many assumptions.
All the services need to be already instantiated, and they must be reliable. It
ties you to some implementation details which can become hard to refactor in
the future. For instance it assumes the backend is a single instance, but how
do you create it? How can you provide the same instance to all the modules that
require it? Singleton pattern?
Also, what if you need different backend services on different environments?
Where do you put the statement to control which service to use? It may depend
on several factors and it can become tricky to make the right choice.
Essentially, the Login view is asking for a login functionality that when
called needs to decide which implementation to use, based on the environment.
This may seem fine, but there is a better way.
So how do we solve those problems?
Level-up the dependencies
The React documentation keeps repeating that the best way for a component to interact with its parent is to level-up the state. If the state is managed by the parent and passed to the component as prop some interactions become trivial.
The question here is: is there any kind of interaction?
In my opinion, there is: the Login view interacts with something in order to
get a login method and call it. It looks like it’s static because the login
function does not impact the render of the Login component during its
lifecycle, but it’s not as it changes its behaviour.
Following this approach, we can pass the login function as a prop to the Login view. This way we level-up the responsibility to provide the right login function to another component up in the hierarchy. Well… we are not inventing anything new, it’s dependency injection the React way, there are other articles about it.
One of the drawbacks of this approach is that the login needs to be defined somewhere else, and it makes a lot of sense to define it somewhere near the top of the hierarchy. Thus, login needs to be passed to all the components from the top down to the Login view…
Luckily, a feature can help: the context. I said drawback because of how the React documentation warns not to use it, but personally I don’t see it as a problem.
First, it only warns of possible changes in the API in the future, but still,
it’s there. If they did not want me to use it they would not have mentioned it
in the first place and they would have called it “_context” or something else.
Second, mainstream libraries like React Connect and React Router heavily use
context. It will not go anywhere soon and if it does, there will be a
replacement otherwise all the ecosystem will be gone.
Third, it is now well documented in many places and all the libraries seem to
follow the same pattern.
The real benefit is that the whole application is now just a function which
takes some dependencies as input, performs some computations, perhaps
asynchronously, and renders views with their own lifecycle.
Basically, bootstrapping the application becomes a controlled process.
Isomorphic applications are much easier to implement, and many things are
simpler when we will introduce routing - see below.
A possible implementation
So why this blog post if it has already been documented?
There are a few libraries to do dependency injection this way, the most
popular being React Redux although it only passes the Redux store.
I was not very satisfied with the existing choices (I mean other non mainstream
libraries passing all the dependencies, not the specialized ones) so I
implemented mine, as the pattern to use context, even if discouraged, is very
well documented.
If you are interested, it’s called
relogic and here is the
source
The idea is fairly simple: everything not enclosed in the lifecycle of a
component can be either enclosed in the lifecycle of a parent component or is
completely unrelated to any view.
As a consequence, I need a way to separate the code unrelated to the views and
put it into an object passed down via the context.
I called this object memory
as it can be instructed with pieces of logic
at any time, even after the view is rendered, and can be asked to expose
this logic later.
When I started implementing it I wanted a mechanism to
- keep all the dependencies and application specific code in one single place, the logic
- be able to use the logic itself as a dependency of other objects
- keep a modular approach
- bootstrap the application with the logic and pass it down with the context
- be able to add logic at runtime and get the logic in an easy way
- introduce a step before adding new logic (more on that on part 2)
- prevent me to override the logic by mistake
- and most importantly control all the dependencies by myself
So I started with a createMemory
function which returns a memory
object,
a simple closure that hides the logic
.
At the beginning the logic
is an empty object and memory
exposes
getLogic
: returns a copy of the logicsetLogic
: merges the logic passed to it with the existing logic.
Then, the same way as other libraries, I needed a wrapper and a consumer to pass the context to the components. So I copied the pattern to have
<Relogic memory />
withLogic((memory, props) => LogicProps)(Component)
You can look at the complete API if you want.
I am still thinking if I can do it simpler, but for now there are a getter, a setter, a provider and a consumer that know absolutely nothing about the logic. They only pass it from one place to another.
So what are the benefits?
Flow of logic
Initially, I wanted to call the library reflow but the name was already
taken :)
The point is, this approach not only gives dependency injection but it makes
possible to reason about the behaviour of the application, at a higher level.
Let’s take the login example, and here is an implementation with relogic
/// index.jsx
// import all the react stuff
import Login from 'path/to/Login/component/file';
// import the login function dependencies
import fetchCreate from 'path/to/backend';
import rememberToken from 'path/to/localStorage';
import history from 'path/to/history';
// import the login logic, responsible for creating the login function
import loginLogic from 'path/to/login/logic';
const memory = createMemory();
memory.setLogic({
login: loginLogic({ fetchCreate, rememberToken, history })
});
ReactDOM.render(
<Relogic memory={memory}>
// deeply nested components down to ...
<Login />
</Relogic>,
document.getElementById('root')
);
/// Login.jsx
class Login {
// Login implementation, but now accepts login as prop...
}
export default withLogic(
({ getLogic }) => {
return {
login: getLogic().login
};
}
);
// loginLogic.js
export default ({ fetchCreate, rememberToken, history }) => {
return {
async login(name, password) {
const { token } = await fetchCreate(
'authentication',
{ name, password }
);
rememberToken(token);
history.goToEntry();
};
};
}
The login function is now injected in the Login view. This solves points
2 and 3 mentioned earlier with the import approach.
And to create the login function, we use a function that receives the
dependencies as arguments. Point 1 solved as now we can test this function in
isolation and we don’t need to stub any more, we are free to replace those
arguments in the first place.
A good term for the login function may be flow of logic, and the good thing is that now, I can replace the dependencies in my tests, have better test coverage and concentrate on integration tests.
relogic is an implementation and the important part is the concept behind.
Having flows of logic leads to better separation of concerns and does not tight
a component to anything unrelated to it.
You may have noticed I did not take an example involving a store. This is to make it clear that this approach has nothing to do with state management. It’s just about flow control.
Integration with redux
This part is optional. It presents a way to go a bit further with relogic.
We saw in the previous paragraph how to define flows of logic. What I did not
say is that we can now treat all the dependencies at the same level. And the
logic itself can be a dependency.
Nothing is more important than anything else, and you can control the
interactions between the dependencies.
What does this imply?
Let’s take the example of store-centric applications. They usually dispatch
actions related to the store to take care of what the store considers a
“side-effect”.
const fetchUsers = () => {
return dispatch => {
fetchGet('users')
.then(users => dispatch(actionReceive(users)))
.then(users => dispatch(actionSelect(users[0])));
};
}
So we need a thunk to place the fetchGet
in. Isn’t this a strong architectural
decision to assume?
Sure, since 2.1.0, Redux Thunk supports injecting a custom argument so we
don’t have the import problem any more, or we could have used a Saga to do the
same thing.
But my question is: why should I restrict myself with the limited interactions a dependency offers me when I can go one level up and control the flow?
Here is the same fetchUsers
const fetchUsers = ({ store, fetchGet }) => {
fetchGet('users')
.then(users => store.dispatch(actionReceive(users)))
.then(users => store.dispatch(actionSelect(users[0])));
};
Now, I am not saying the store should not be the source of truth.
And I won’t. The store is the source of truth.
It’s just that to me, what the store considers “side-effects”, well, they are
not. They are at the core of my application, and I would rather make sure I
control them because usually, it is where I find all the critical
things (think cookies, storage, security, network…).
You can think of relogic as a library with dependency injection, control of flow, and if you want to, roughly replaces the second argument of the connect function.
What else? “idempotent” routes?
When it comes to routing, what you may want is just pass a url, and as a result
have a bunch of operations, perhaps asynchronous, producing some rendering.
I will come back to this in a moment as we may have already introduced a
paradox.
Let’s say when hitting the route /users
we want to fetch a list of users and
display it.
The first approach and usually the documented one is to have a component,
<Users />
, that when mounted will fetch the users.
Can this be improved?
Let’s have a look at what could be done with React Router.
The documentation
says it is possible to pass a function to the onEnter
prop of a
<Route />
, this function being is called when a route is about to be
entered.
Here is the example from the documentation
const userIsInATeam = (nextState, replace, callback) => {
fetch(...)
.then(response = response.json())
.then(userTeams => {
if (userTeams.length === 0) {
replace(`/users/${nextState.params.userId}/teams/new`)
}
callback();
})
.catch(error => {
// do some error handling here
callback(error);
})
}
<Route path="/users/:userId/teams" onEnter={userIsInATeam} />
What the documentation keeps vague is fetch(...)
. Actually, those 3 dots are
the the left-alone land, the dependencies.
It looks a lot like a flow to me. How can we inject the dependencies?
Instead of directly calling the <Router />
component from the library we can
create one and inject the dependency into it.
// Router.jsx
export default ({ memory }) => {
const { users, history, routes } = memory.getLogic();
const userIsInATeam = (nextState, replace, callback) => {
const { arg1, arg2, ... } = users.resolveFetchArgumentsFor(nextState);
fetch(arg1, arg2, ...)
// rest of implementation
};
return (
<Router history={ history }>
<Route path={ routes.userTeam } onEnter={ userIsInATeam } />
</Router>
);
};
// index.jsx
// assume everything is imported
const memory = createMemory();
memory.setLogic({
users: {
resolveFetchArgumentsFor: state => {
// ...
return { arg1, arg2, ... };
}
},
history: { ... },
routes: {
userTeam: '...'
}
});
ReactDOM.render(
<Relogic memory={memory}>
<Router memory={memory} />
</Relogic>,
document.getElementById('root')
);
We have just passed the dependencies of userIsInATeam
as a piece of logic.
What does it mean? Why does it matter?
Now we have an “idempotent” route. Anytime we hit this route, we can expect the same behaviour, because we control how the application is bootstrapped.
It is worth mentioning routes and history can be dependencies too.
Although react-router provides history objects, we may want to enhance them
to add some logic.
For instance, back to the login example, we may want to add the ability to
redirect to the last visited page after login.
const PROPERTY_LAST_VISITED = 'relogic-demo-last-visited';
const historyStorage = ({ localStorage, routes }) => {
return {
getLastVisited: () => {
const lastVisited = localStorage.getItem(PROPERTY_LAST_VISITED);
return lastVisited && JSON.parse(lastVisited) || routes.dashboard;
},
setLastVisited: (path) => {
localStorage.setItem(
PROPERTY_LAST_VISITED,
JSON.stringify(path)
);
}
};
};
const applicationHistory = ({ history }) => {
goToEntry: () => { history.push(history.getLastVisited()); }
};
const enhanceHistory = ({ history, localStorage }) => {
return Object.assign({},
history,
historyStorage({ history, localStorage }),
applicationHistory({ history })
);
};
As a result, the login flow that calls history.goToEntry
will redirect to the
last visited page. We could have added any kind of logic, it would have been
applied the same way.
The flow does not care of the operations involved, it just delegates to the
dependencies.
Integrating Redux and React Router together
Now, maybe you remember what I wrote at the start of the previous paragraph:
when it comes to routing you may want to start with an url and then have
some computations to eventually render views, and this may introduce a
paradox.
The problem arises if you want to combine Redux and React Router together and
the paradox is that Redux is not your source of truth any more.
Wait… everything I have read on the internet so far is clear, the state is
the source of truth. So how am I supposed to deal with that?
Part 2 of the article shows an approach that basically ignores the conflict and by using flow control leverages the advantages of both libraries. It also shows a possible reason why other libraries which try to fill the gaps, namely Redux Router and React Router Redux are over-complicating the problem.
As a proof of concept, part 2 shows how it is possible to implement basic time travelling with both Redux and React Router using flows of logic. Because now you can control your flows, so you can always decide not to run them, right?
Conclusion
I hope this reading was interesting and added a small contribution.
We have have discussed the advantages of using dependency injection with React, we have seen a possible implementation, and we have introduced the concept of flow of logic. Then we have applied the flow to some popular libraries, Redux and React Router, leveraging the power of both of them without their restrictions by simply doing flow control.
Nothing is new, we have just applied an old pattern: level-up the problem. It is not the most fancy solution, but it simplifies the code and makes testing much easier.
There is a demo where other flows
are defined.
And in part 2, there will be much more.