DEV Community

Cover image for Write your own authorization hook using React Stores – a shared state manipulating library for React and React Native
Ruslan Shashkov
Ruslan Shashkov

Posted on

Write your own authorization hook using React Stores – a shared state manipulating library for React and React Native

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.

  1. Using react hooks.
  2. Using my tiny but very powerful shared stores library React Stores.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

Here we created a few things:

  1. Exported store instance that we can use everywhere in the app.
  2. The interface for the store that strictly declares store contents.
  3. 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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
/* 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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
// 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>
  );
};
Enter fullscreen mode Exit fullscreen mode
// AuthorizePage.tsx
import React from "react";

export const AuthorizePage = () => (
  <div>
    <h1>Authorize</h1>
    <button>Press to login</button>
  </div>
);
Enter fullscreen mode Exit fullscreen mode
// HomePage.tsx
import React from "react";

export const HomePage = () => (
  <div>
    <h1>Home</h1>
    <p>Welcome!</p>
  </div>
);
Enter fullscreen mode Exit fullscreen mode
// PrivatePage.tsx
import React from "react";

export const PrivatePage = () => (
  <div>
    <h1>Private page</h1>
    <p>Wake up, Neo...</p>
  </div>
);
Enter fullscreen mode Exit fullscreen mode
// PublicPage.tsx
import React from "react";

export const PublicPage = () => (
  <div>
    <h1>Public page</h1>
    <p>Nothing special here</p>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

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);
  ...
}
Enter fullscreen mode Exit fullscreen mode

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>
...
Enter fullscreen mode Exit fullscreen mode

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
});
...
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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
  });
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
...
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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
  }
);
Enter fullscreen mode Exit fullscreen mode

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.

🤓🦄❤️

Online demo

Top comments (0)