目次
- 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の特徴
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 と定義します
{
"compilerOptions": {
"sourceMap": true,
"target": "es5",
"module": "es2015",
"jsx": "react",
"moduleResolution": "node",
"lib": [
"es2020",
"dom"
]
}
}
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を定義しましょう。
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も作成します。
<!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の部分に以下の記述を追加します。
{
"scripts": {
"start": "webpack-dev-server",
},
}
ターミナルから npm start と入力すると開発サーバーが立ち上がるはずです。
4. unstated-nextをインストールします。
npm install -S unstated-next
5. 状態を保持するためのContainerを定義します。
試しにUser情報を保持・更新するためのUserContainerを定義してみましょう。ファイルの場所は 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を編集していきます。
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を呼び出します。
// 例
const userContainer = useContainer(UserContainer)
userContainer.user
// => { name: "" }
userContainer.setUser({ name: "Tom"} )
userContainer.name
// => { name: "Tom" }
ここで出てくる nameとsetNameは、先程UserContainer内のuseUserの返り値のオブジェクトです。
const useUser = () => {
const [user, setUser] = useState<User>(initialUser);
// この返り値のオブジェクトがuseContainer(userContainer)に返される。
return { user, setUser }
};
うごくものができました!
Tips
ここからはより実践的なunstated-nextの使い方について書きます。
複数のContainerを定義する。
1つのContainerだけで状態管理をしているとContainerが肥大化して可読性,保守性が下がります。そんな時はContainerを分割してしまいましょう。
新たにPlanContainerを定義します。
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にアクセス可能になります。
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の呼び出し元のネストが重なり、いらいらしてきます。
<User.Provider>
<Auth.Provider>
<Plan.Provider>
<EventLog.Provider>
// ... ネスト地獄
</EventLog.Provider>
</Plan.Provider>
</Auth.Provider>
</UserProvider>
unstated-nextは現時点ではこれに対する解決策を用意していませんが
自作関数を定義することで可読性を改善することができます。
以下のcomposeという関数は、配列として与えられたContainerをネストしたJSXとして返却するシンプルな関数です。
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 を定義します。
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から呼び出せば晴れて(見た目的には)ネスト解消です。
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を叩いた後の処理などで使うケースがあると思います。
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分程度で理解できると思います。せっかくなのでソースコードも目を通しておくと勉強になります。