準備

Create React App でプロジェクトディレクトリを作成。

$ npx create-react-app my-todo
$ cd my-todo
$ npm start

Unstated Next をインストール

$ npm install --save unstated-next

今回使用するバージョンは下記の通りです。

“react”: “^16.8.6”,
“unstated-next”: “^1.0.1”

コンテナの作成

コンテナは共通で使用するStateや関数を入れます。
最後にオブジェクトで返します。

src/components/TaskContainer.js

import { useState } from 'react';

export default () => {
	const [count, setCount] = useState(1);
    const [items, setItems] = useState([]);

	const handleAdd = (e) => {
		e.preventDefault();

		const newItems = [...items, {
			id: count,
			title: e.target.title.value,
			done: false
		}];

		setItems(newItems);
		setCount(count + 1);

		e.target.title.value = '';
	}

	const handleDone = (item) => {
		const newItems = [...items];
		let index = newItems.indexOf(item);
		item.done = !item.done;
		newItems[index] = item;

		setItems(newItems);
	}

	return { count, items, handleAdd, handleDone };
}

useStateを使うとthis.stateのような書き方をしなくていいのでシンプルに書けますね。

Task.js

中心となるコンポーネントです。
createContainerをexportして子のコンポーネントで共通のコンテナを使用できるようにします。
コンテナを使用するコンポーネントをTaskContainer.Providerで囲みます。

src/components/Task.js

import React from 'react';
import { createContainer } from "unstated-next";
import useTask from './TaskContainer';
import TaskList from './TaskList';
import TaskInput from './TaskInput';

export const TaskContainer = createContainer(useTask);

export default () => {
	return (
		<TaskContainer.Provider>
			<TaskInput />
			<TaskList />
		</TaskContainer.Provider>
	);
};

TaskInput.js

入力用コンポーネントです。
TaskContainerを読み込んでuseContainerすると、container.handleAddのような形で、リターンしたステートや関数にアクセスできます。

src/components/TaskInput.js

import React from 'react';
import { TaskContainer } from './Task';

export default () => {
    const container = TaskContainer.useContainer();

    return(
        <form onSubmit={container.handleAdd} className="task-input">
            <input type="text" name="title" />
            <button>追加</button>
        </form>
    );
};

TaskList.js

一覧表示用のコンポーネントです。
TaskInput.jsとやってることは変わらないですね。

src/components/TaskInput.js

import React from 'react';
import { TaskContainer } from './Task';

export default () => {
	const container = TaskContainer.useContainer();

	return(
		<ul>
			{
				container.items.map((item) => {
					return(
						<li
							key={item.id}
							onClick={() => container.handleDone(item)}
							className={`${item.done ? "done" : ""}`}
						>{item.title}</li>
					);
				})
			}
		</ul>
	);
};

App.js

App.jsで起点となるTaskコンポーネントを配置します。

src/App.js

import React from 'react';
import Task from './components/Task'
import './App.css';

function App() {
	return (
	<div className="App">
		<Task />
	</div>
	);
}

export default App;

最後にスタイルを作って完成です。
今回はdoneクラスが付与されたとき、打ち消し線が引かれるという簡易的なものです。

src/App.css

.done {
	text-decoration: line-through;
}

以上。
React Hooks と Unstated Next の組み合わせでかなりシンプルに書けるようになる気がします。

jamiebuilds/unstated-next