React使用组件的方式和单向数据流使其非常适合描述用户界面的结构。 但是,它的用于处理状态的工具故意保持简单—有助于提醒我们,React只是传统的Model-View-Controller体系结构中的View 。
没有什么可以阻止我们仅使用React来构建大型应用程序,但是我们很快就会发现,为了保持代码简单,我们需要在其他地方管理状态。
尽管没有用于处理应用程序状态的官方解决方案,但仍有一些库特别符合React的范例。 在本文中,我们将React与两个这样的库配对,并使用它们来构建一个简单的应用程序。
Redux
Redux是一个微型库,通过结合Flux和Elm的想法,充当我们应用程序状态的容器。 只要遵守以下准则,我们就可以使用Redux来管理任何类型的应用程序状态:
- 我们的状态保存在单个商店中
- 变化来自行动而不是突变
Redux存储的核心是一个函数,该函数采用当前应用程序状态和一个动作,并将它们组合以创建新的应用程序状态。 我们称这个函数为reducer 。
我们的React组件负责将操作发送到我们的商店,然后我们的商店将告诉组件何时需要重新渲染。
不可变JS
由于Redux不允许我们更改应用程序状态,因此通过使用不可变的数据结构对应用程序状态进行建模可以强制实施此操作。
ImmutableJS为我们提供了许多具有可变接口的不可变数据结构,它们的有效实现是受Clojure和Scala中实现的启发。
演示版
我们将使用React和Redux和ImmutableJS来构建一个简单的待办事项列表,允许我们添加待办事项并在完整和不完整之间切换。
请参阅CodePen上的SitePoint ( @SitePoint )的Pen React,Redux和不可变Todo 。
设定
我们将通过创建项目文件夹并使用npm init
初始化package.json
文件开始。 然后,我们将安装所需的依赖项。
npm install --save react react-dom redux react-redux immutable
npm install --save-dev webpack babel-core babel-loader babel-preset-es2015 babel-preset-react
我们将使用JSX和ES2015 ,因此将使用Babel编译代码,并将其作为Webpack模块捆绑过程的一部分进行。
首先,我们将在webpack.config.js
创建Webpack配置:
module.exports = {
entry: './src/app.js',
output: {
path: __dirname,
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: { presets: [ 'es2015', 'react' ] }
}
]
}
};
最后,我们将通过添加一个npm脚本来扩展package.json
,以使用源映射编译我们的代码:
"script": {
"build": "webpack --debug"
}
每次我们要编译代码时,都需要运行npm run build
。
反应和组件
在实现任何组件之前,创建一些虚拟数据可能会有所帮助。 这有助于我们了解需要使用哪些组件来呈现:
const dummyTodos = [
{ id: 0, isDone: true, text: 'make components' },
{ id: 1, isDone: false, text: 'design actions' },
{ id: 2, isDone: false, text: 'implement reducer' },
{ id: 3, isDone: false, text: 'connect components' }
];
对于这个应用程序,我们只需要两个React组件, <Todo />
和<TodoList />
。
// src/components.js
import React from 'react';
export function Todo(props) {
const { todo } = props;
if(todo.isDone) {
return <strike>{todo.text}</strike>;
} else {
return <span>{todo.text}</span>;
}
}
export function TodoList(props) {
const { todos } = props;
return (
<div className='todo'>
<input type='text' placeholder='Add todo' />
<ul className='todo__list'>
{todos.map(t => (
<li key={t.id} className='todo__item'>
<Todo todo={t} />
</li>
))}
</ul>
</div>
);
}
此时,我们可以通过在项目文件夹中创建index.html
文件并使用以下标记填充它们来测试这些组件。 (您可以在GitHub上找到一个简单的样式表):
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
<title>Immutable Todo</title>
</head>
<body>
<div id="app"></div>
<script src="bundle.js"></script>
</body>
</html>
我们还将需要一个应用程序入口点src/app.js
// src/app.js
import React from 'react';
import { render } from 'react-dom';
import { TodoList } from './components';
const dummyTodos = [
{ id: 0, isDone: true, text: 'make components' },
{ id: 1, isDone: false, text: 'design actions' },
{ id: 2, isDone: false, text: 'implement reducer' },
{ id: 3, isDone: false, text: 'connect components' }
];
render(
<TodoList todos={dummyTodos} />,
document.getElementById('app')
);
使用npm run build
编译代码,然后将浏览器导航到index.html
文件,并确保其正常工作。
Redux和不可变
现在,我们对用户界面感到满意,我们可以开始考虑其背后的状态。 我们的虚拟数据是一个很好的起点,我们可以轻松地将其转换为ImmutableJS集合:
import { List, Map } from 'immutable';
const dummyTodos = List([
Map({ id: 0, isDone: true, text: 'make components' }),
Map({ id: 1, isDone: false, text: 'design actions' }),
Map({ id: 2, isDone: false, text: 'implement reducer' }),
Map({ id: 3, isDone: false, text: 'connect components' })
]);
ImmutableJS映射的工作方式与JavaScript对象不同,因此我们需要对组件进行一些细微调整。 之前需要访问属性的任何地方(例如todo.id
)都需要变为方法调用( todo.get('id')
)。
设计动作
现在我们已经弄清楚了形状和结构,我们可以开始考虑将对其进行更新的操作。 在这种情况下,我们只需要执行两项操作,一项添加新的待办事项,另一项切换现有的待办事项。
让我们定义一些函数来创建这些动作:
// src/actions.js
// succinct hack for generating passable unique ids
const uid = () => Math.random().toString(34).slice(2);
export function addTodo(text) {
return {
type: 'ADD_TODO',
payload: {
id: uid(),
isDone: false,
text: text
}
};
}
export function toggleTodo(id) {
return {
type: 'TOGGLE_TODO',
payload: id
}
}
每个动作只是一个具有类型和有效负载属性的JavaScript对象。 type属性可帮助我们决定以后处理操作时如何处理有效负载。
设计减速器
现在,我们知道状态的形状以及对其进行更新的操作,我们可以构建减速器。 提醒一下,reduceer是一个函数,它接受一个状态和一个动作,然后使用它们来计算新的状态。
这是我们的减速器的初始结构:
// src/reducer.js
import { List, Map } from 'immutable';
const init = List([]);
export default function(todos=init, action) {
switch(action.type) {
case 'ADD_TODO':
// …
case 'TOGGLE_TODO':
// …
default:
return todos;
}
}
处理ADD_TODO
动作非常简单,因为我们可以使用.push()方法,该方法将返回一个新列表,并在其末尾附加待办事项:
case 'ADD_TODO':
return todos.push(Map(action.payload));
注意,在将todo对象推送到列表之前,我们还将其转换为不可变的映射。
我们需要处理的更复杂的操作是TOGGLE_TODO
:
case 'TOGGLE_TODO':
return todos.map(t => {
if(t.get('id') === action.payload) {
return t.update('isDone', isDone => !isDone);
} else {
return t;
}
});
我们正在使用.map()遍历列表,并找到id
与操作匹配的待办事项。 然后,我们调用.update() ,它使用一个键和一个函数,然后返回映射的新副本,该键处的值替换为将初始值传递给update函数的结果。
查看文字版本可能会有所帮助:
const todo = Map({ id: 0, text: 'foo', isDone: false });
todo.update('isDone', isDone => !isDone);
// => { id: 0, text: 'foo', isDone: true }
连接一切
现在我们已经准备好动作和reducer,我们可以创建一个商店并将其连接到我们的React组件:
// src/app.js
import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { TodoList } from './components';
import reducer from './reducer';
const store = createStore(reducer);
render(
<TodoList todos={store.getState()} />,
document.getElementById('app')
);
我们需要使我们的组件知道此商店。 我们将使用react-redux来简化此过程。 它允许我们创建包装在组件周围的存储感知容器,因此我们不必更改原始的实现。
我们将需要在<TodoList />
组件周围的容器。 让我们看看它是什么样的:
// src/containers.js
import { connect } from 'react-redux';
import * as components from './components';
import { addTodo, toggleTodo } from './actions';
export const TodoList = connect(
function mapStateToProps(state) {
// …
},
function mapDispatchToProps(dispatch) {
// …
}
)(components.TodoList);
我们使用connect函数创建容器。 当我们调用connect()
,我们传递了两个函数mapStateToProps()
和mapDispatchToProps()
。
mapStateToProps
函数将商店的当前状态作为参数(在我们的示例中是待办事项列表),然后期望返回值是一个对象,该对象描述了从该状态到我们包装的组件的props的映射:
function mapStateToProps(state) {
return { todos: state };
}
在包装好的React组件的实例上可视化它可能会有所帮助:
<TodoList todos={state} />
我们还需要提供mapDispatchToProps
函数,该函数将通过商店的dispatch
方法传递,以便我们可以使用它来分派动作创建者的动作:
function mapDispatchToProps(dispatch) {
return {
addTodo: text => dispatch(addTodo(text)),
toggleTodo: id => dispatch(toggleTodo(id))
};
}
再次,这可能有助于在包装好的React组件的实例上将所有这些道具可视化:
<TodoList todos={state}
addTodo={text => dispatch(addTodo(text))}
toggleTodo={id => dispatch(toggleTodo(id))} />
现在我们已经将组件映射到动作创建者,我们可以从事件监听器中调用它们:
export function TodoList(props) {
const { todos, toggleTodo, addTodo } = props;
const onSubmit = (event) => {
const input = event.target;
const text = input.value;
const isEnterKey = (event.which == 13);
const isLongEnough = text.length > 0;
if(isEnterKey && isLongEnough) {
input.value = '';
addTodo(text);
}
};
const toggleClick = id => event => toggleTodo(id);
return (
<div className='todo'>
<input type='text'
className='todo__entry'
placeholder='Add todo'
onKeyDown={onSubmit} />
<ul className='todo__list'>
{todos.map(t => (
<li key={t.get('id')}
className='todo__item'
onClick={toggleClick(t.get('id'))}>
<Todo todo={t.toJS()} />
</li>
))}
</ul>
</div>
);
}
容器将自动订阅商店中的更改,并且一旦映射的道具发生更改,它们就会重新渲染包装的组件。
最后,我们需要使用<Provider />
组件使容器知道商店:
// src/app.js
import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducer from './reducer';
import { TodoList } from './containers';
// ^^^^^^^^^^
const store = createStore(reducer);
render(
<Provider store={store}>
<TodoList />
</Provider>,
document.getElementById('app')
);
结论
不可否认,React和Redux周围的生态系统可能非常复杂,并且对初学者构成威胁,但是好消息是,几乎所有这些概念都是可以移植的。 我们几乎没有触及Redux架构的表面,但是已经看到足够的知识来帮助我们开始学习Elm架构 ,或者选择Om或Re-frame之类的ClojureScript库。 同样,我们只看到了不可变数据的可能性的一小部分,但是现在我们有了更好的条件来开始学习Clojure或Haskell这样的语言。
无论您是只是探索Web应用程序的开发状态,还是整日编写JavaScript,基于动作的体系结构和不可变数据的经验都已成为开发人员的一项重要技能,而现在正是学习该应用程序的绝佳时机。要领。
From: https://www.sitepoint.com/how-to-build-a-todo-app-using-react-redux-and-immutable-js/