Help us understand the problem. What is going on with this article?

unstated-nextでシンプルなReact状態管理【React/TypeScriptハンズオン付き】

目次

  • unstated-nextとは
  • unstated-nextの特徴
  • 10分ハンズオン(React.js / TypeScript / unstated-next)
  • Tips
    • 複数のContainerを定義する
    • Containerが増えたときネスト地獄を避ける
    • 非同期処理
  • 参考文献

対象読者

・redux触ってみて記述量の多さに圧倒されてしまったReact初心者
・ReactHooks大好きお兄さんお姉さん。

また、Reactを始めたばかりでHooksを触ったことがない人でもとりあえず動かせるようにハンズオンも容易しています。

unstated-nextとは

React HooksベースはReactの状態管理ライブラリです。

状態管理ライブラリといえば、reduxを筆頭にmobx, unduxなど様々なライブラリがありますが、その中でもこのunstated-nextの際立った特徴はそのシンプルさです。なんとソースコードが38行しかありません。それゆえに学習コストが非常に低く(*注1)、カスタマイズもしやすいというのがこのライブラリの最大の特徴だとおもっています。

個人的な複雑な状態管理を必要としない小規模のツール系のアプリならばReduxではなくこのunstated-nextをファーストチョイスとして良いと思うほど気に入っているライブラリです。

※注1: React Hooksを理解している前提となります

unstated-next
Image from Gyazo

unstated-nextの特徴

Pros

・ソースコードが非常に小さいので全容の把握が容易(全38行)
・従って学習コストが非常に低い
・書かなければいけないコードの行数が少ない
・React Hooksの薄い薄いラッパーなのでカスタマイズしやすい

Cons

・複雑な状態管理, 大規模アプリに向いているかは(筆者には)わからない。
・React Hooksを学ぶための学習コストは必要。Class構文に慣れている人は少し疲れるかも。

10分ハンズオン

このハンズオンではReactの環境構築から行っていきます。

ソースコードはこちら。commitログをたどると手順がわかると思います。

最終的なディレクトリ構造は以下のようになります。

- dist
    - index.html
- src
    - containers
        - compose.tsx
        - GeneralContainer.ts
        - UserContainer.ts
        - PlanContainer.ts
    - main.tsx
- package-lock.json
- package.json
- tsconfig.json
- webpack.config.js

0. npmでReact/TypeScriptの開発に必要なライブラリを入れていきます。

mkdir unstated-next-sample
cd unstated-next-sample
npm init -y
npm i -D webpack webpack-cli webpack-dev-server typescript ts-loader
npm i -S react react-dom @types/react @types/react-dom

1. tsconfig.json と webpack.config.js と定義します

tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es5",
    "module": "es2015",
    "jsx": "react",
    "moduleResolution": "node",
    "lib": [
      "es2020",
      "dom"
    ]
  }
}
webpack.config.js
module.exports = {
  mode: "development",

  entry: "./src/main.tsx",
  output: {
    path: `${__dirname}/dist`,
    filename: "main.js"
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader"
      }
    ]
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"]
  },
  devServer: {
    contentBase: `${__dirname}/dist`
  }
};

2. 簡単なReactComponentを定義しましょう。

src/main.tsxに簡単なReactComponentを定義しましょう。

src/main.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';

const App: React.FC = (props) => {
  return (
    <div>
      <h1>UnstatedNext!</h1>
    </div>
  )
}

+const app = document.getElementById("app")
+ReactDOM.render(<App />, app)

dist/index.htmlも作成します。

dist/index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>UnstatedNext</title>
</head>

<body>
  <div id="app"></div>
  <script src="main.js"></script>
</body>

</html>

3. webpack-dev-serverを起動しましょう。

package.jsonのscriptの部分に以下の記述を追加します。

package.json
{
  "scripts": {
    "start": "webpack-dev-server",
  },
}

ターミナルから npm start と入力すると開発サーバーが立ち上がるはずです。

4. unstated-nextをインストールします。

npm install -S unstated-next

5. 状態を保持するためのContainerを定義します。

試しにUser情報を保持・更新するためのUserContainerを定義してみましょう。ファイルの場所は src/containers/UserContainer.ts とします。

src/containers/UserContainer.ts
import { useState } from 'react';
import { createContainer } from 'unstated-next';

type User = {
  name: string
}
const initialUser: User = { name: "" };

const useUser = () => {
  const [user, setUser] = useState<User>(initialUser);

  return { user, setUser }
};

const UserContainer = createContainer(useUser);

export { UserContainer }

なんとunstated-nextはこれだけで状態管理が可能になります。

6. Containerを呼び出してみよう

ではUserContainerの状態にアクセスするために、少しmain.tsxを編集していきます。

src/main.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { useContainer } from 'unstated-next';
import { UserContainer } from "./containers/UserContainer"

const App: React.FC = (props) => {
  return (
    <UserContainer.Provider>
      <Screen />
    </UserContainer.Provider>
  )
}

const Screen: React.FC = (props) => {
  const userContainer = useContainer(UserContainer)

  const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    const user = { ...userContainer.user, name: e.target.value }
    userContainer.setUser(user);
  }

  return (
    <div>
      <h1>UnstatedNext!</h1>
      <p>{userContainer.user.name}</p>
      <input onChange={onChangeInput} />
    </div>
  )
}

const app = document.getElementById("app")
ReactDOM.render(<App />, app)

でラップされた配下のコンポーネントで
Containerの情報を使うことができます。

あるコンポーネントからContainerにアクセスしたい場合はuseContainerという関数を使い利用したいContainerを呼び出します。

src/main.tsx
// 例
const userContainer = useContainer(UserContainer)

userContainer.user
// => { name: "" }

userContainer.setUser({ name: "Tom"} )

userContainer.name
// => { name: "Tom" }

ここで出てくる nameとsetNameは、先程UserContainer内のuseUserの返り値のオブジェクトです。

src/containers/UserContainer.ts
const useUser = () => {
  const [user, setUser] = useState<User>(initialUser);

  // この返り値のオブジェクトがuseContainer(userContainer)に返される。
  return { user, setUser }
};

うごくものができました!

Image from Gyazo

Tips

ここからはより実践的なunstated-nextの使い方について書きます。

複数のContainerを定義する。

1つのContainerだけで状態管理をしているとContainerが肥大化して可読性,保守性が下がります。そんな時はContainerを分割してしまいましょう。

新たにPlanContainerを定義します。

src/containers/PlanContainer.ts
import { useState } from 'react';
import { createContainer } from 'unstated-next';

type Plan = {
  planID: string,
  planName: string,
}

const initialPlan: Plan = { planID: "1234567", planName: "Basic Plan" };

const usePlan = () => {
  const [plan, setPlan] = useState<Plan>(initialPlan);

  return { plan, setPlan }
};

const PlanContainer = createContainer(usePlan);

export { PlanContainer }

呼び出しもとで行うことは非常に簡単です。
Providerをネストして書くだけで複数のcontainerにアクセス可能になります。

main.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { useContainer } from 'unstated-next';
import { GeneralContainer } from "./containers/GeneralContainer";
import { UserContainer } from "./containers/UserContainer"
import { PlanContainer } from "./containers/PlanContainer";

const App: React.FC = (props) => {
  return (
    <UserContainer.Provider>
      <PlanContainer.Provider> // 追加!
        <Screen />
      </PlanContainer.Provider>
    </UserContainer.Provider>
  )
}

const Screen: React.FC = (props) => {
  const userContainer = useContainer(UserContainer)
  const planContainer = useContainer(PlanContainer) // アクセスできる!

  const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    const user = { ...userContainer.user, name: e.target.value }
    userContainer.setUser(user);
  }

  return (
    <div>
      <h1>UnstatedNext!</h1>
      <p>{userContainer.user.name}</p>
      <input onChange={onChangeInput} />
      <p>Your Plan is {planContainer.plan.planName}({planContainer.plan.planID})</p>
    </div>
  )
}

const app = document.getElementById("app")
ReactDOM.render(<App />, app)

Containerが増えたときネスト地獄を避ける

複数のContainerを利用しようとするとProviderの呼び出し元のネストが重なり、いらいらしてきます。

example.tsx
<User.Provider>
  <Auth.Provider>
    <Plan.Provider>
      <EventLog.Provider>
         // ... ネスト地獄
      </EventLog.Provider>
    </Plan.Provider>
  </Auth.Provider>
</UserProvider>

unstated-nextは現時点ではこれに対する解決策を用意していませんが
自作関数を定義することで可読性を改善することができます。

以下のcomposeという関数は、配列として与えられたContainerをネストしたJSXとして返却するシンプルな関数です。

compose.tsx
import * as React from "react";
import { Container } from "unstated-next";

const compose = (containers: Array<Container<any, any>>, children: JSX.Element): JSX.Element => {
  return containers.reduce((acc, Container) => {
    return <Container.Provider>{acc}</Container.Provider>
  }, children)
}

export { compose }

この関数を使って、すべてのContainerをまとめる GeneralContainer を定義します。

src/containers/GeneralContainer.tsx
import { compose } from "./compose";

import { UserContainer } from "./UserContainer";
import { PlanContainer } from "./PlanContainer";

const GeneralContainer = (props): JSX.Element => {
  return (
    compose(
      [
        UserContainer,
        PlanContainer
      ],
      props.children
    )
  )
}

export { GeneralContainer }

これをmain.tsxから呼び出せば晴れて(見た目的には)ネスト解消です。

src/main.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { useContainer } from 'unstated-next';
import { GeneralContainer } from "./containers/GeneralContainer";
import { UserContainer } from "./containers/UserContainer"
import { PlanContainer } from "./containers/PlanContainer";

const App: React.FC = (props) => {
  return (
    <GeneralContainer>
      <Screen />
    </GeneralContainer>
  )
}

const Screen: React.FC = (props) => {
  const userContainer = useContainer(UserContainer)
  const planContainer = useContainer(PlanContainer)

  const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    const user = { ...userContainer.user, name: e.target.value }
    userContainer.setUser(user);
  }

  return (
    <div>
      <h1>UnstatedNext!</h1>
      <p>{userContainer.user.name}</p>
      <input onChange={onChangeInput} />
      <p>Your Plan is {planContainer.plan.planName}({planContainer.plan.planID})</p>
    </div>
  )
}

const app = document.getElementById("app")
ReactDOM.render(<App />, app)

非同期処理

Reduxには非同期処理を行うためのミドルウェアとしてredux-sagaやredux-sagaがありますが、unstated-nextで非同期処理を行いたい場合はcontainer内に自前で非同期関数を定義することで実現できます。

APIを叩いた後の処理などで使うケースがあると思います。

src/main.tsx
import { useState } from 'react';
import { createContainer } from 'unstated-next';

type User = {
  name: string
}

const initialUser: User = { name: "" };

const useUser = () => {
  const [user, setUser] = useState<User>(initialUser);

  // 2秒後に更新する関数
  const setUserAsync = (user: User) => {
    return new Promise((resolve) => {
      setTimeout(
        () => {
          setUser(user)
          resolve()
        },
        2000);
    });
  }

  return { user, setUser, setUserAsync }
};

const UserContainer = createContainer(useUser);

export { UserContainer }

参考文献

ReactHooks公式
公式が非常にわかりやすく網羅的なDocs, Tutorialを用意してくれているので、まだHooksに触ったことが無い方は目を通しましょう。手を動かしても賞味1時間程度で終わります。

様々な種類のHooksがありますが、ひとまずのところ以下の3つだけ理解しておけば大丈夫だと思います。

  • useState
  • useEffect
  • useContext

unstated-next
冒頭にも書きましたがソースコードの時点で38行しかないため、Documentも非常にシンプルです。この記事を読んだあとであれば5分程度で理解できると思います。せっかくなのでソースコードも目を通しておくと勉強になります。

moris5321
株式会社NOIABという会社でエンジニアをしています。「音楽のインフラ」を目指して目下サービス開発中です。エンジニア募集中。 仕事でよくつかう → Ruby / Elixir / TypeScript / React 個人的に好きで書く → Haskell / Rust / C
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした