The Problem: Imagine that you have an app that has multiple states global to all your components. How do you distribute these states to the components that require them and how do you manage manipulating it? inferno-context-api-store
allows you to do that with ease!
Prerequisites to be able to follow along
You can verify oif your NodeJS
has been installed by doing the following:
npm --version
node --version
You should see the version of your npm
and NodeJS
.
Scope
In this post, I’ll cover the following:
- Creating inferno app.
- Running the app.
- Setting up
inferno-context-api-store
’s store. - Setting up the pages/routes.
- Navigating around the site
- Connecting components to the store.
- Adding and dispatching action handlers and updateStore callback.
- Persisting states.
What is inferno-context-api-store?
inferno-context-api-store
is a state management library that is compatible with InfernoJS
. It offers easy and simple to use API and it was inspired by both Redux and Vuex. Except that inferno-context-api-store
uses ReactJS’ Context API to manage the store. React's Context API
is not avaiable in inferno out of the box, instead, there’s a different package for it called create-inferno-context.
inferno-context-api-store
also supports persistent states
and asynchronous actions
out of the box.
Creating an Inferno App
First, install create-inferno-app package:
npm install --global create-inferno-app
Once that’s done, you can now create your project by executing the following commands:
mkdir ~/Desktop/projects
cd ~/Desktop/projects
create-inferno-app my-app-name
On the commands above, we created a folder called projects
on your Desktop
. Then we cd
(change directory) onto that directory and we executed create-inferno-app my-app-name
which will create the application for us. The app will be called my-app-name
in this case but you can change this according to your preference.
What create-inferno-app
is really doing is what other developers call bootstraping or simply getting started in the fastest possible way. When you executed the command create-inferno-app my-app-name
you run a script that will create the directories and files and it will also install all the necessary dependencies that will help you get started.
Running the app
Once the app has been installed, you can run the app by executing the following commands:
cd my-app-name
npm install --save inferno-context-api-store
Once the install is done, you can run the app by executing npm start
.
You should see an output similar to the following:
Compiled successfully!
You can now view test-app in the browser.
Local: http://localhost:3000/
On Your Network: http://192.168.1.9:3000/
Note that the development build is not optimized.
To create a production build, use yarn build.
Now you can go to http://localhost:3000/
, or whatever is shown in your terminal/CMD, and you will see the inferno default homepage.
To get started, look at the src/App.js
or whatever inferno is telling you in the homepage.
You should see something like this:
import './registerServiceWorker';
import { version, Component } from 'inferno';
import Logo from './logo';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<Logo width="80" height="80" />
<h1>{`Welcome to Inferno ${version}`}</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
export default App;
Did you notice the HTML
inside the JavaScript?
That’s called JSX. You don’t have to worry about it for now, just think of it as the ability to write HTML inside of your Inferno app.
Experiencing any errors?
Try to delete the package-lock.json
file on the root directory. Then do npm install
. Once done, do npm start
again.
Now to install react-context-api-store
.
- On your terminal, hit
ctrl + c
. - type in
npm install --save react-context-api-store
.
The Provider
In order to create a store, you must use what’s called a Provider
. Which is basically an HOC
that initializes the store. You must use it as the top most component of your app.
Step 1
First, we import it from the inferno-context-api-store
package like so:
import Provider from 'inferno-context-api-store';
Step 2
Modify the app by making the Provider
the top most component like so:
const store = {
todos: [],
username: 'April'
};
class App extends Component {
render() {
return (
<Provider store={store}>
<div className="App">
<header className="App-header">
<Logo width="80" height="80" />
<h1>{`Welcome to Inferno ${version}`}</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
</Provider>
);
}
}
Notice that we passed in a prop
called store
. It is a required prop, and it contains all your application’s global states. In this case, we have the todos
and the username
which has a default value of April (but in more complex apps you can have many).
The store
is nothing especial, it’s just an object and that’s about it. You create a function that generates the initial states
for you or whatever. As long as the end result is an object and you pass that object to the Provider
.
Now your store is all setup. Yeah, it’s as easy as that.
Setting up the pages/routes
First, let’s set up the pages we need, in this example, we’ll only use two pages. One page to allow the user to change his username
and another to allow the user to add new stuff to his todos
.
Step 1
To do this, we’ll need the help of a router
. We’ll use the package inferno-router. You can install it by executing npm install inferno-router
.
Step 2
Modify the src/App.js
to be like so:
import { Component } from 'inferno';
import { HashRouter, Switch, Route } from 'inferno-router';
import Provider from 'inferno-context-api-store';
const store = {
todos: [],
username: 'April'
};
class App extends Component {
render() {
return (
<Provider store={store}>
<HashRouter>
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/todos" exact={true} component={Todos} />
</Switch>
</HashRouter>
</Provider>
);
}
}
export default App;
I know there are a lot of other files there, but we are not really going to use any css here, until the last part of the tutorial. Right now, what we really need are the src/App.js
and src/index.js
, you can safely delete the rest from this point.
Now, when you refresh the page you’ll see an error saying something like 'Todos' is not defined no-undef
, this is the desired output, we’ll fix it in the following step.
Step 3
Create a folder called Home
inside the src
folder, then create a file called index.js
inside the src/Home
folder.
The index.js
file will be the Home component
, for the mean time, it’s just a regular component.
import { Component } from 'inferno';
export default class Homepage extends Component {
render () {
return (
<h1>Homepage!</h1>
);
}
}
Step 4
Create a folder called Todos
inside the src
folder, then create a file called index.js
inside the src/Todos
folder.
The index.js
file will be the Todos component
, for the mean time, it’s just a regular component.
import { Component } from 'inferno';
export default class Todospage extends Component {
render () {
return (
<h1>Todos!</h1>
);
}
}
Step 5
On the src/App.js
,
after:
import Provider from 'inferno-context-api-store';
and before:
const store = {
todos: [],
username: 'April'
};
add the following lines:
import Home from './Home';
import Todos from './Todos';
If you refresh the page, you should see the Homepage! in h1
tag. Then you can go to http://localhost:3000/#/todos
and you’ll see Todos!. This means that you have done well.
Navigating around the site
Of course we don’t want to navigate between pages by typing the url on the url bar. We need something that we can click on and navigate to where we want WITHOUT RELOADING THE WHOLE PAGE.
You must be thinking of using an anchor
tag, that will work, but remember that we don’t want to reload the whole page, that’s the whole point of SPAs
. We’ll use a module from inferno-router
called the Link
, which is like an anchor
tag that serves as a link that the user can click to navigate between pages, except it does not reload the whole page.
Modify the JSX
in src/Home/index.js
file, to look like so:
<div>
<h1>Homepage!</h1>
<Link to="/todos">Go to Todos</Link>
</div>
To be able to use the Link
module, we also need to import it from inferno-router
like so:
import { Link } from 'inferno-router';
Do the same for src/Todos/index.js
, but instead, the to
property of the Link
would be equal to /
like so:
<Link to="/">Go to Home</Link>
Now, when you refresh the page you’ll see an anchor
tag that you can click on and you’ll be directed to the corresponding page. Notice that the page does not refresh when you navigate between pages.
Now we don’t want to hard code our navigation links in every pages that we will have, so, what we’ll do is add that on to the App.js
file before the Switch
. So modify the JSX
of that file like so:
<Provider store={store}>
<HashRouter>
<div>
<Link to="/todos">Go to Todos</Link> | <Link to="/">Go to Home</Link>
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/todos" exact={true} component={Todos} />
</Switch>
</div>
</HashRouter>
</Provider>
Don’t forget this too!
import { Link } from 'inferno-router';
Before you refresh the site, remove the previous changes that we did in both src/Home/index.js
and src/Todos/index.js
related to the Link
modules.
Now we have our navigation above just before the actual page content.
Connecting a component to the store
To do this, we’ll need the help of the connect
module from inferno-context-api-store
. Modify the src/Home/index.js
file like so:
import { Component } from 'inferno';
import { connect } from 'inferno-context-api-store';
class Homepage extends Component {
render () {
return (
<div>
<h1>Homepage!</h1>
</div>
);
}
}
export default connect(state => {
console.log('the state has:', state);
return {};
})(Homepage);
Open up the console by right clicking on the page then click on inspect element, then click on the console
tab on the upper side of the screen. Refesh the page and you’ll see that there’s a message there that says something like this:
'the state has:' {todos: Array(0), username: "April"}
The connect
module is a function that accepts two parameters.
mapStateToProps
, is basically a callback that receives the store’s updated state and returns an object coming from that state. The component then receives all these asprop
. We only returned an empty object for now.mapActionsToProps
is an object that functions. The component will receive these functions asprop
and can call them at will. Each function is called anaction handler
and receivesstore
as it’s first parameter. Thestore
is an object that contains two things.store.state
which is the store’s updated store as of the moment the function was called.-
store.updateStore
which is a function that you will call whenever you want to update a part of the store. It accepts anobject
, which is the states that you want to update, as it’s first parameter and an optional second parameter which is acallback function
that will receive the store’s updated state as it’s first parameter.NOTE: You should not update the state directly in your component by mutating it because it will not reflect onto other connected components, instead you should always fire an action and call
store.updateStore
.
Now, change the App.js
, only the import part and replace it with these:
export default connect(state => ({
username: state.username,
todos: state.todos
}))(Homepage);
Here, we are passing a mapStateToProps function
to connect
and we are immediately returning an object
. Now, the Homepage
component should receive these as part of its props
. Let’s do a console log
to see the component’s props
. On the render function
, add this before the return
statement.
console.log(this.props.username);
Now if you refresh the page, you should see your username in the console.
Let’s display the user’s username on the homepage. Change the JSX
like so:
<div>
<h1>Hello, {this.props.username}!</h1>
</div>
Now you should see the Hello, April! or whatever username you have.
Action handlers
The connect
module can accept a second parameter that is an object with methods. These methods will serve as your action handlers. You use them to update the store’s state.
Now let’s modify the src/Home/index.js
to have a action handler called changeUsername
. but before that, on our Homepage class
, let’s add a new method called handleChangeUsername
which will handle changing username.
handleChangeUsername = ev => {
console.log(ev.target.target);
}
Then on the JSX, add the following on the next line after the h1
tag.
<p>Want to change your username?</p>
<input
type="text"
value={this.props.username}
onInput={this.handleChangeUsername} />
If you refresh and check your console, you’ll see that as you type you get the value of the username
with the new key that you just pressed, however, the value of the input
itself does not change, this is what’s called two way data binding
where you keep the value of the UI consistent with the value of the state
, in this case, username
, which is a state in our store. So how do we change the value of the state? Well, we need to update the store and we do that via action handlers.
Let’s add our changeUsername
action handler. Replace the whole export default connect
line with the following:
function changeUsername (store, updatedUsername) {
console.log(store, updatedUsername);
}
export default connect(state => ({
username: state.username,
todos: state.todos
}), {
changeUsername
})(Homepage);
Then on the handleChangeUsername
method, do the following:
handleChangeUsername = ev => {
this.props.changeUsername(ev.target.value);
}
If you refresh the page and check your console, you’ll see the value of the store
and the updatedUsername
. Note that ALL action handlers will receive the store
as its first argument
, the rest of the arguments
will be whatever you passed to it, in this case we only have one, which is the ev.target.value
, which we received and chose to call as updatedUsername
. You can pass as many arguments
to your action handlers
as you need, but make sure to keep in mind that you will receive the store first, then the rest.
You can update the store
’s state by calling the updateStore
method on the store
. Let’s do that:
function changeUsername (store, updatedUsername) {
store.updateStore({
username: updatedUsername
});
}
In our case, we want to update the username
state in the store. Now as you type on the input text, your username
should change. The method updateStore
can also accept an option second argument, which is a callback function
, which will receive the store's updated state
like so:
function callback (updatedState) {
console.log(updatedState);
}
function changeUsername (store, updatedUsername) {
store.updateStore({
username: updatedUsername
}, callback);
}
There are many ways to add action handlers
to your components. It can also be like this:
function callback (updatedState) {
console.log(updatedState);
}
export default connect(state => ({
username: state.username,
todos: state.todos
}), {
changeUsername (store, updatedUsername) {
store.updateStore({
username: updatedUsername
}, callback);
}
})(Homepage);
or like this:
function callback (updatedState) {
console.log(updatedState);
}
export default connect(state => ({
username: state.username,
todos: state.todos
}), {
changeUsername: (store, updatedUsername) => store.updateStore({
username: updatedUsername
}, callback)
})(Homepage);
How you do it is totally up to you.
Persisting states
The Provider
component can accept an optional property called persist
which should be an object that has the following properties:
storage
. The storage of your choice, It should be synchronous and it should offer the following API:getItem
, which accepts the key as the first argument.setItem
, which accepts the key as the first argument and the value as the second argument.removeItem
, which accepts the key as the first argument.
statesToPersist
. A function that would be called when initializing the store. It accepts thesavedState
, which came from the storage, as the first argument. It should return an object of the states you want to persist and their values.
Now let’s change our App.js
’s render method
to be like so:
const persist = {
storage: window.localStorage,
statesToPersist (savedState) {
/**
* here, we are saying that if we have a
* username stored on our savedState, we'll use that,
* otherwise we'll use the value of store.username which is
* the initial value
*/
return {
username: savedState.username || store.username
};
}
};
class App extends Component {
render() {
return (
<Provider
store={store}
persist={persist}>
<HashRouter>
<div>
<Link to="/todos">Go to Todos</Link> | <Link to="/">Go to Home</Link>
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/todos" exact={true} component={Todos} />
</Switch>
</div>
</HashRouter>
</Provider>
);
}
}
Now if you refresh the page and change your username, then refresh the page again, you’ll see that your username is retained.
The entire source code of the example project is available at https://github.com/aprilmintacpineda/inferno-context-api-store/tree/master/example. Please do check it out.
Other links related to inferno-context-api-store
Corrections of any sort are always welcome. Thanks.