Hello everyone! In this tutorial, I would like to show you how to learn three powerful techniques that I use to build great React and React Native applications with TypeScript.
- Using react hooks.
- Using my tiny but very powerful shared stores library React Stores.
- Making protected routes with React Router 5.x with those techniques.
So let's begin.
Initializing project
Open your terminal and initialize a new React application (let's use Create React App). Don't forget --typescript
flag for use TypeScript boilerplate during the creation of our application.
create-react-app my-app --typescript
cd my-app
Okay, we've just initialized our CRA, now its time to run. I prefer to use yarn
but you can choose your favorite package manager.
yarn start
Then open your browser and go to http://localhost:3000.
Yay! Now we have our shiny new app is up and running!
Commit #1. See on GitHub.
Installing dependencies
Let's install react-stores library and react-router-dom with its TypeScript definitions:
yarn add react-stores react-router-dom @types/react-router-dom
Now we're ready to create our first shared store. Let's do it. Create file store.ts
inside src
directory:
// store.ts
import { Store } from "react-stores";
interface IStoreState {
authorized: boolean;
}
export const store = new Store<IStoreState>({
authorized: false
});
Here we created a few things:
- Exported store instance that we can use everywhere in the app.
- The interface for the store that strictly declares store contents.
- And passed initial state values (actually only one value here
authorized
, but you can put as much as you need).
Commit #2. See on GitHub.
Routes and navigation
Nothing special here, just create simple routing with React Dom Router.
// App.tsx
import React from "react";
import { BrowserRouter, Route, Switch, Link } from "react-router-dom";
const HomePage = () => (
<div>
<h1>Home</h1>
<p>Welcome!</p>
</div>
);
const PublicPage = () => (
<div>
<h1>Public page</h1>
<p>Nothing special here</p>
</div>
);
const PrivatePage = () => (
<div>
<h1>Private page</h1>
<p>Wake up, Neo...</p>
</div>
);
const App: React.FC = () => {
return (
<BrowserRouter>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/public">Public</Link>
</li>
<li>
<Link to="/private">Private</Link>
</li>
</ul>
<Switch>
<Route exact path="/" component={HomePage} />
<Route exact path="/public" component={PublicPage} />
<Route exact path="/private" component={PrivatePage} />
</Switch>
</BrowserRouter>
);
};
export default App;
Now we have simple SPA with a few routes and navigation.
Commit #3. See on GitHub.
Adding some complexity
Here we add a header with navigation, new authorization route and fake Login/Exit button, plus some simple CSS-styles.
// App.tsx
import React from "react";
import { BrowserRouter, Route, Switch, NavLink } from "react-router-dom";
import "./index.css";
const HomePage = () => (
<div>
<h1>Home</h1>
<p>Welcome!</p>
</div>
);
const PublicPage = () => (
<div>
<h1>Public page</h1>
<p>Nothing special here</p>
</div>
);
const PrivatePage = () => (
<div>
<h1>Private page</h1>
<p>Wake up, Neo...</p>
</div>
);
const AuthorizePage = () => (
<div>
<h1>Authorize</h1>
<button>Press to login</button>
</div>
);
const App: React.FC = () => {
return (
<BrowserRouter>
<header>
<ul>
<li>
<NavLink exact activeClassName="active" to="/">
Home
</NavLink>
</li>
<li>
<NavLink exact activeClassName="active" to="/public">
Public
</NavLink>
</li>
<li>
<NavLink exact activeClassName="active" to="/private">
Private
</NavLink>
</li>
</ul>
<ul>
<li>
<NavLink exact activeClassName="active" to="/authorize">
Authorize
</NavLink>
</li>
</ul>
</header>
<main>
<Switch>
<Route exact path="/" component={HomePage} />
<Route exact path="/public" component={PublicPage} />
<Route exact path="/private" component={PrivatePage} />
<Route exact path="/authorize" component={AuthorizePage} />
</Switch>
</main>
</BrowserRouter>
);
};
export default App;
/* index.css */
body {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
}
header {
background-color: #eee;
padding: 10px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
main {
padding: 30px;
}
ul {
list-style: none;
padding: 0;
display: flex;
align-items: center;
}
ul li {
margin-right: 30px;
}
a {
text-decoration: none;
color: #888;
}
a.active {
color: black;
}
Commit #4. See on GitHub.
Using React Stores in components
Now its time to add simple authorization logic and use our Store to see it in action.
Separating components into files
First, let's move our navigation and pages components into separate files for code separation, it's a good practice 😊.
// App.tsx
import React from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import "./index.css";
import { Nav } from "./Nav";
import { HomePage } from "./HomePage";
import { PublicPage } from "./PublicPage";
import { PrivatePage } from "./PrivatePage";
import { AuthorizePage } from "./AuthorizePage";
const App: React.FC = () => {
return (
<BrowserRouter>
<Nav />
<main>
<Switch>
<Route exact path="/" component={HomePage} />
<Route exact path="/public" component={PublicPage} />
<Route exact path="/private" component={PrivatePage} />
<Route exact path="/authorize" component={AuthorizePage} />
</Switch>
</main>
</BrowserRouter>
);
};
export default App;
// Nav.tsx
import React from "react";
import { NavLink } from "react-router-dom";
export const Nav: React.FC = () => {
return (
<header>
<ul>
<li>
<NavLink exact activeClassName="active" to="/">
Home
</NavLink>
</li>
<li>
<NavLink exact activeClassName="active" to="/public">
Public
</NavLink>
</li>
<li>
<NavLink exact activeClassName="active" to="/private">
Private
</NavLink>
</li>
</ul>
<ul>
<li>
<NavLink exact activeClassName="active" to="/authorize">
Authorize
</NavLink>
</li>
</ul>
</header>
);
};
// AuthorizePage.tsx
import React from "react";
export const AuthorizePage = () => (
<div>
<h1>Authorize</h1>
<button>Press to login</button>
</div>
);
// HomePage.tsx
import React from "react";
export const HomePage = () => (
<div>
<h1>Home</h1>
<p>Welcome!</p>
</div>
);
// PrivatePage.tsx
import React from "react";
export const PrivatePage = () => (
<div>
<h1>Private page</h1>
<p>Wake up, Neo...</p>
</div>
);
// PublicPage.tsx
import React from "react";
export const PublicPage = () => (
<div>
<h1>Public page</h1>
<p>Nothing special here</p>
</div>
);
Commit #5. See on GitHub.
Using store state
Now time to add shared states to our components. The first component will be Nav.tsx
. We will use built-in React hook from react-stores
package – useStore()
.
// Nav.tsx
...
import { store } from './store';
import { useStore } from 'react-stores';
export const Nav: React.FC = () => {
const authStoreState = useStore(store);
...
}
Now our Nav
component is bound to the Store through useStore()
hook. The component will update each time store updates. As you can see this hook is very like usual useState(...)
from the React package.
Next, let's use authorized
property from the Store state. To render something depends on this property. For example we can render conditional text in Authorize navigation link in our navigation.
// Nav.tsx
...
<li>
<NavLink exact activeClassName='active' to='/authorize'>
{authStoreState.authorized ? 'Authorized' : 'Login'}
</NavLink>
</li>
...
Now, text inside this link depends on authorized
property. You can try now to change the initial store state to see how "Login" changes to "Authorized" in our Nav.tsx
when you set its value from false
to true
and vice versa.
// store.ts
...
export const store = new Store<IStoreState>({
authorized: true, // <-- true or false here
});
...
Next, we're going to change AuthorizePage.tsx
to bind it to our Store and set another one conditional rendering by useState()
hook.
// AuthorizePage.tsx
import React from "react";
import { useStore } from "react-stores";
import { store } from "./store";
export const AuthorizePage: React.FC = () => {
/*
You must pass exactly that store instance, that you need to use.
Because you can have multiple stores in your app of course.
*/
const authStoreState = useStore(store);
return authStoreState.authorized ? (
<div>
<h1>Authorized</h1>
<button>Press to exit</button>
</div>
) : (
<div>
<h1>Unauthorized</h1>
<button>Press to login</button>
</div>
);
};
You can play with the initial state to see how page /authorize
changes depending on the Store. 🤪
Commit #6. See on GitHub.
Mutating store
Now it's time to implement our authorization flow. It will be a simple function, but that's will be enough to show the concept.
And of course, you can write your own auth flow, for example, fetch some data from a server to get a token or some login-password authentication mechanism it does not important.
Our functions just toggles the authorized
boolean value.
Create file authActions.ts
:
// authActions.ts
import { store } from "./store";
export function login() {
store.setState({
authorized: true
});
}
export function logout() {
store.setState({
authorized: false
});
}
As you can see, here we call the Store instance setState()
method to mutate its state and update all the components bound to the Store.
Now we can bind auth button to authActions
.
// AuthorizePage.tsx
...
import { login, logout } from './authActions';
...
return authStoreState.authorized ? (
<div>
<h1>Authorized</h1>
<button onClick={logout}>Press to logout</button>
</div>
) : (
<div>
<h1>Unauthorized</h1>
<button onClick={login}>Press to login</button>
</div>
);
...
That's it... For now. You can try to navigate to /authorize
and click the Login/Logout button to see it in action. The page and Navigation should update to show your current authorization state each time you toggle.
Custom hook
Time to write your custom hook. Let's call it useProtectedPath
. Its purpose is to check the current browser's location path, compare it to a given protected paths list and return a boolean value: true
if path protected and the user is authorized, otherwise false
, or if path is not in protected, return true
whether user authorized or not.
So, create a file useProtectedPath.ts
.
import { useStore } from "react-stores";
import { store } from "./store";
import { useRouteMatch } from "react-router";
const PROTECTED_PATHS = ["/private"];
export const useProtectedPath = () => {
const { authorized } = useStore(store);
const match = useRouteMatch();
const protectedPath =
PROTECTED_PATHS.indexOf((match && match.path) || "") >= 0;
const accessGrant = !protectedPath || (protectedPath && authorized);
return accessGrant;
};
After that you can use it in PrivatePage.tsx
like that:
import React from "react";
import { useProtectedPath } from "./useProtectedPath";
import { Redirect } from "react-router";
export const PrivatePage = () => {
const accessGrant = useProtectedPath();
if (!accessGrant) {
return <Redirect to="/authorize" />;
}
return (
<div>
<h1>Private page</h1>
<p>Wake up, Neo...</p>
</div>
);
};
Now your /private
page will redirect you to /authorize
to let you authorize.
That's it we've made it!
🥳🥳🥳
Commit #7. See on GitHub.
Bonus
Try this snippet in your store.ts
. Then authorize and reload the page in the browser. As you can see, your authorized state will be restored. That means your Store now has a persistent state from session to session.
// store.ts
export const store = new Store<IStoreState>(
{
authorized: false
},
{
persistence: true // This property does the magic
}
);
React Stores support persistence. That's mean that you can store your store state in Local Storage by default, or even make your own driver, for example, IndexedDB or Cookies, or even a network fetcher as a Driver. See readme and sources on https://github.com/ibitcy/react-stores#readme.
...And never use LocalStorage to store your token or other sensitive data in the Local Storage. This article uses a Local Storage driver for persist authorization state only for the explanation of the concept. 😶
One more thing... You can make Time Machine functionality via creating snapshots of your states. You can see how it works here: https://ibitcy.github.io/react-stores/#Snapshots.
Thank you for reading. I hope it will help someone to create something great and with ease.
🤓🦄❤️
Top comments (0)