feat: add 'back/front end' in curriculum (#42596)
* chore: rename APIs and Microservices to include "Backend" (#42515) * fix typo * fix typo * undo change * Corrected grammar mistake Corrected a grammar mistake by removing a comma. * change APIs and Microservices cert title * update title * Change APIs and Microservices certi title * Update translations.json * update title * feat(curriculum): rename apis and microservices cert * rename folder structure * rename certificate * rename learn Markdown * apis-and-microservices -> back-end-development-and-apis * update backend meta * update i18n langs and cypress test Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * fix: add development to front-end libraries (#42512) * fix: added-the-word-Development-to-front-end-libraries * fix/added-the-word-Development-to-front-end-libraries * fix/added-word-development-to-front-end-libraries-in-other-related-files * fix/added-the-word-Development-to-front-end-and-all-related-files * fix/removed-typos-from-last-commit-in-index.md * fix/reverted-changes-that-i-made-to-dependecies * fix/removed xvfg * fix/reverted changes that i made to package.json * remove unwanted changes * front-end-development-libraries changes * rename backend certSlug and README * update i18n folder names and keys * test: add legacy path redirect tests This uses serve.json from the client-config repo, since we currently use that in production * fix: create public dir before moving serve.json * fix: add missing script * refactor: collect redirect tests * test: convert to cy.location for stricter tests * rename certificate folder to 00-certificates * change crowdin config to recognise new certificates location * allow translations to be used Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com> * add forwards slashes to path redirects * fix cypress path tests again * plese cypress * fix: test different challenge Okay so I literally have no idea why this one particular challenge fails in Cypress Firefox ONLY. Tom and I paired and spun a full build instance and confirmed in Firefox the page loads and redirects as expected. Changing to another bootstrap challenge passes Cypress firefox locally. Absolutely boggled by this. AAAAAAAAAAAAAAA * fix: separate the test Okay apparently the test does not work unless we separate it into a different `it` statement. >:( >:( >:( >:( Co-authored-by: Sujal Gupta <55016909+heysujal@users.noreply.github.com> Co-authored-by: Noor Fakhry <65724923+NoorFakhry@users.noreply.github.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com>
This commit is contained in:
@ -0,0 +1,156 @@
|
||||
---
|
||||
id: 5a24c314108439a4d4036147
|
||||
title: 连接 Redux 和 React
|
||||
challengeType: 6
|
||||
forumTopicId: 301426
|
||||
dashedName: connect-redux-to-react
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
你已经写了`mapStateToProps()`、`mapDispatchToProps()` 两个函数,现在可以用它们来把 `state` 和 `dispatch` 映射到 React 组件的 `props` 了。 React Redux 的 `connect` 方法可以完成这个任务。 此方法有 `mapStateToProps()`、`mapDispatchToProps()` 两个可选参数, 它们是可选的,原因是你的组件可能仅需要访问 `state` 但不需要分发任何 actions,反之亦然。
|
||||
|
||||
为了使用此方法,需要传入函数参数并在调用时传入组件。 这种语法有些不寻常,如下所示:
|
||||
|
||||
```js
|
||||
connect(mapStateToProps, mapDispatchToProps)(MyComponent)
|
||||
```
|
||||
|
||||
**注意:**如果要省略 `connect` 方法中的某个参数,则应当用 `null` 替换这个参数。
|
||||
|
||||
# --instructions--
|
||||
|
||||
代码编辑器具有 `mapStateToProps()` 和 `mapDispatchToProps()` 函数,以及一个名为 `Presentational` 的新 React 组件。 使用 `ReactRedux` 全局对象中的 `connect` 方法将此组件连接到 Redux,并立即在 `Presentational` 组件上调用它。 将结果赋值给名为 `ConnectedComponent` 的新 `const`,表示连接的组件。 就是这样,现在你已经连接到 Redux 了! 尝试将 `connect` 的参数更改为 `null`,并观察测试结果。
|
||||
|
||||
# --hints--
|
||||
|
||||
应渲染 `Presentational` 组件。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
return mockedComponent.find('Presentational').length === 1;
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`Presentational` 组件应通过 `connect` 接收一个 `messages` 属性。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
const props = mockedComponent.find('Presentational').props();
|
||||
return props.messages === '__INITIAL__STATE__';
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`Presentational` 组件应通过 `connect` 接收一个 `submitNewMessage` 属性。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
const props = mockedComponent.find('Presentational').props();
|
||||
return typeof props.submitNewMessage === 'function';
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
## --after-user-code--
|
||||
|
||||
```jsx
|
||||
const store = Redux.createStore(
|
||||
(state = '__INITIAL__STATE__', action) => state
|
||||
);
|
||||
class AppWrapper extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<ReactRedux.Provider store = {store}>
|
||||
<ConnectedComponent/>
|
||||
</ReactRedux.Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
ReactDOM.render(<AppWrapper />, document.getElementById('root'))
|
||||
```
|
||||
|
||||
## --seed-contents--
|
||||
|
||||
```jsx
|
||||
const addMessage = (message) => {
|
||||
return {
|
||||
type: 'ADD',
|
||||
message: message
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
messages: state
|
||||
}
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
submitNewMessage: (message) => {
|
||||
dispatch(addMessage(message));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class Presentational extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
render() {
|
||||
return <h3>This is a Presentational Component</h3>
|
||||
}
|
||||
};
|
||||
|
||||
const connect = ReactRedux.connect;
|
||||
// Change code below this line
|
||||
```
|
||||
|
||||
# --solutions--
|
||||
|
||||
```jsx
|
||||
const addMessage = (message) => {
|
||||
return {
|
||||
type: 'ADD',
|
||||
message: message
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
messages: state
|
||||
}
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
submitNewMessage: (message) => {
|
||||
dispatch(addMessage(message));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class Presentational extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
render() {
|
||||
return <h3>This is a Presentational Component</h3>
|
||||
}
|
||||
};
|
||||
|
||||
const connect = ReactRedux.connect;
|
||||
// Change code below this line
|
||||
|
||||
const ConnectedComponent = connect(mapStateToProps, mapDispatchToProps)(Presentational);
|
||||
```
|
@ -0,0 +1,300 @@
|
||||
---
|
||||
id: 5a24c314108439a4d4036148
|
||||
title: 将 Redux 连接到 Messages App
|
||||
challengeType: 6
|
||||
forumTopicId: 301427
|
||||
dashedName: connect-redux-to-the-messages-app
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
知道如何使用 `connect` 连接 React 和 Redux 后,我们可以在 React 组件中应用上面学到的内容。
|
||||
|
||||
在上一课,连接到 Redux 的组件命名为 `Presentational`,这个命名不是任意的, 这样的术语*通常*是指未直接连接到 Redux 的 React 组件, 它们只负责执行接收 props 的函数来实现 UI 的呈现。 相比之下,容器组件用来连接到 Redux 上。 这些组件通常负责把 actions 分派给 store,且经常给子组件传入 store state 属性。
|
||||
|
||||
# --instructions--
|
||||
|
||||
到目前为止,我们的编辑器上已包含了整个章节的代码, 唯一不同的是,React 组件被重新命名为 `Presentational`,即展示层组件。 创建一个新组件,保存在名为 `Container` 的常量中。 这个常量用 `connect` 把 `Presentational` 组件和 Redux 连接起来。 然后,在`AppWrapper` 中渲染 React Redux 的 `Provider`组件, 给 `Provider` 传入 Redux `store` 属性并渲染 `Container` 为子组件。 设置完所有内容后,将再次看到消息应用程序渲染到页面上。
|
||||
|
||||
# --hints--
|
||||
|
||||
`AppWrapper` 应渲染该页面上。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
return mockedComponent.find('AppWrapper').length === 1;
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`Presentational` 组件应该渲染到页面上。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
return mockedComponent.find('Presentational').length === 1;
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`Presentational` 组件应渲染 `h2`、`input`、`button`、`ul` 四个元素。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
const PresentationalComponent = mockedComponent.find('Presentational');
|
||||
return (
|
||||
PresentationalComponent.find('div').length === 1 &&
|
||||
PresentationalComponent.find('h2').length === 1 &&
|
||||
PresentationalComponent.find('button').length === 1 &&
|
||||
PresentationalComponent.find('ul').length === 1
|
||||
);
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`Presentational` 组件应接收 Redux store 的 `messages` 属性。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
const PresentationalComponent = mockedComponent.find('Presentational');
|
||||
const props = PresentationalComponent.props();
|
||||
return Array.isArray(props.messages);
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`Presentational` 组件应接收创建 action 的函数 `submitMessage` 属性。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
const PresentationalComponent = mockedComponent.find('Presentational');
|
||||
const props = PresentationalComponent.props();
|
||||
return typeof props.submitNewMessage === 'function';
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
## --after-user-code--
|
||||
|
||||
```jsx
|
||||
ReactDOM.render(<AppWrapper />, document.getElementById('root'))
|
||||
```
|
||||
|
||||
## --seed-contents--
|
||||
|
||||
```jsx
|
||||
// Redux:
|
||||
const ADD = 'ADD';
|
||||
|
||||
const addMessage = (message) => {
|
||||
return {
|
||||
type: ADD,
|
||||
message: message
|
||||
}
|
||||
};
|
||||
|
||||
const messageReducer = (state = [], action) => {
|
||||
switch (action.type) {
|
||||
case ADD:
|
||||
return [
|
||||
...state,
|
||||
action.message
|
||||
];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const store = Redux.createStore(messageReducer);
|
||||
|
||||
// React:
|
||||
class Presentational extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
input: '',
|
||||
messages: []
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.submitMessage = this.submitMessage.bind(this);
|
||||
}
|
||||
handleChange(event) {
|
||||
this.setState({
|
||||
input: event.target.value
|
||||
});
|
||||
}
|
||||
submitMessage() {
|
||||
this.setState((state) => {
|
||||
const currentMessage = state.input;
|
||||
return {
|
||||
input: '',
|
||||
messages: state.messages.concat(currentMessage)
|
||||
};
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Type in a new Message:</h2>
|
||||
<input
|
||||
value={this.state.input}
|
||||
onChange={this.handleChange}/><br/>
|
||||
<button onClick={this.submitMessage}>Submit</button>
|
||||
<ul>
|
||||
{this.state.messages.map( (message, idx) => {
|
||||
return (
|
||||
<li key={idx}>{message}</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// React-Redux:
|
||||
const mapStateToProps = (state) => {
|
||||
return { messages: state }
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
submitNewMessage: (newMessage) => {
|
||||
dispatch(addMessage(newMessage))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Provider = ReactRedux.Provider;
|
||||
const connect = ReactRedux.connect;
|
||||
|
||||
// Define the Container component here:
|
||||
|
||||
|
||||
class AppWrapper extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
render() {
|
||||
// Complete the return statement:
|
||||
return (null);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
# --solutions--
|
||||
|
||||
```jsx
|
||||
// Redux:
|
||||
const ADD = 'ADD';
|
||||
|
||||
const addMessage = (message) => {
|
||||
return {
|
||||
type: ADD,
|
||||
message: message
|
||||
}
|
||||
};
|
||||
|
||||
const messageReducer = (state = [], action) => {
|
||||
switch (action.type) {
|
||||
case ADD:
|
||||
return [
|
||||
...state,
|
||||
action.message
|
||||
];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const store = Redux.createStore(messageReducer);
|
||||
|
||||
// React:
|
||||
class Presentational extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
input: '',
|
||||
messages: []
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.submitMessage = this.submitMessage.bind(this);
|
||||
}
|
||||
handleChange(event) {
|
||||
this.setState({
|
||||
input: event.target.value
|
||||
});
|
||||
}
|
||||
submitMessage() {
|
||||
this.setState((state) => {
|
||||
const currentMessage = state.input;
|
||||
return {
|
||||
input: '',
|
||||
messages: state.messages.concat(currentMessage)
|
||||
};
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Type in a new Message:</h2>
|
||||
<input
|
||||
value={this.state.input}
|
||||
onChange={this.handleChange}/><br/>
|
||||
<button onClick={this.submitMessage}>Submit</button>
|
||||
<ul>
|
||||
{this.state.messages.map( (message, idx) => {
|
||||
return (
|
||||
<li key={idx}>{message}</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// React-Redux:
|
||||
const mapStateToProps = (state) => {
|
||||
return { messages: state }
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
submitNewMessage: (newMessage) => {
|
||||
dispatch(addMessage(newMessage))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Provider = ReactRedux.Provider;
|
||||
const connect = ReactRedux.connect;
|
||||
|
||||
const Container = connect(mapStateToProps, mapDispatchToProps)(Presentational);
|
||||
|
||||
class AppWrapper extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Container/>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
@ -0,0 +1,397 @@
|
||||
---
|
||||
id: 5a24c314108439a4d4036149
|
||||
title: 将局部状态提取到 Redux 中
|
||||
challengeType: 6
|
||||
forumTopicId: 301428
|
||||
dashedName: extract-local-state-into-redux
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
马上就完成了! 请回顾一下为管理 React messages app 的状态写的 Redux 代码。 现在有了连接好的 Redux,还要从`Presentational`组件中提取状态管理到 Redux, 目前,已连接 Redux,但正在 `Presentational` 组件中本地处理状态。
|
||||
|
||||
# --instructions--
|
||||
|
||||
在 `Presentational` 组件中,先删除本地 `state` 中的 `messages` 属性, 被删的 messages 将由 Redux 管理。 接着,修改 `submitMessage()` 方法,使该方法从 `this.props` 那里分发 `submitNewMessage()`;从本地 `state` 中传入当前消息输入作为参数。 因本地状态删除了 `messages` 属性,所以在调用 `this.setState()` 时也要删除 `messages` 属性。 最后,修改 `render()` 方法,使其所映射的消息是从 `props` 接收的,而不是 `state`
|
||||
|
||||
完成这些更改后,我们的应用会实现 Redux 管理应用的状态,但它继续运行着相同的功能。 此示例还阐明了组件获得本地 `state` 的方式,即在自己的 `state` 中继续跟踪用户本地输入。 由此可见,Redux 为 React 提供了很有用的状态管理框架。 先前,仅使用 React 的本地状态也实现了相同的结果,这在应付简单的应用时通常是可行的。 但是,随着应用变得越来越大,越来越复杂,应用的状态管理也变得非常困难,Redux 就是为解决这样的问题而诞生的。
|
||||
|
||||
# --hints--
|
||||
|
||||
`AppWrapper` 应该渲染该到页面上。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
return mockedComponent.find('AppWrapper').length === 1;
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`Presentational` 应该渲染到页面上.
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
return mockedComponent.find('Presentational').length === 1;
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`Presentational` 组件应渲染 `h2`、`input`、`button`、`ul` 四个元素。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
const PresentationalComponent = mockedComponent.find('Presentational');
|
||||
return (
|
||||
PresentationalComponent.find('div').length === 1 &&
|
||||
PresentationalComponent.find('h2').length === 1 &&
|
||||
PresentationalComponent.find('button').length === 1 &&
|
||||
PresentationalComponent.find('ul').length === 1
|
||||
);
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`Presentational` 组件应接收 Redux store 的 `messages` 属性。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
const PresentationalComponent = mockedComponent.find('Presentational');
|
||||
const props = PresentationalComponent.props();
|
||||
return Array.isArray(props.messages);
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`Presentational` 组件应接收创建 action 的函数的 `submitMessage` 属性。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
const PresentationalComponent = mockedComponent.find('Presentational');
|
||||
const props = PresentationalComponent.props();
|
||||
return typeof props.submitNewMessage === 'function';
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`Presentational` 组件的状态应包含一个初始化为空字符串的 `input` 属性。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
const PresentationalState = mockedComponent
|
||||
.find('Presentational')
|
||||
.instance().state;
|
||||
return (
|
||||
typeof PresentationalState.input === 'string' &&
|
||||
Object.keys(PresentationalState).length === 1
|
||||
);
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
键入 `input` 元素应更新 `Presentational` 组件的状态。
|
||||
|
||||
```js
|
||||
async () => {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
const testValue = '__MOCK__INPUT__';
|
||||
const waitForIt = (fn) =>
|
||||
new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100));
|
||||
const causeChange = (c, v) =>
|
||||
c.find('input').simulate('change', { target: { value: v } });
|
||||
let initialInput = mockedComponent.find('Presentational').find('input');
|
||||
const changed = () => {
|
||||
causeChange(mockedComponent, testValue);
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const updated = await changed();
|
||||
const updatedInput = updated.find('Presentational').find('input');
|
||||
assert(
|
||||
initialInput.props().value === '' &&
|
||||
updatedInput.props().value === '__MOCK__INPUT__'
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
在 `Presentational` 组件上 dispatch `submitMessage` 应更新 Redux store 并清除本地状态中的输入。
|
||||
|
||||
```js
|
||||
async () => {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
const waitForIt = (fn) =>
|
||||
new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100));
|
||||
let beforeProps = mockedComponent.find('Presentational').props();
|
||||
const testValue = '__TEST__EVENT__INPUT__';
|
||||
const causeChange = (c, v) =>
|
||||
c.find('input').simulate('change', { target: { value: v } });
|
||||
const changed = () => {
|
||||
causeChange(mockedComponent, testValue);
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const clickButton = () => {
|
||||
mockedComponent.find('button').simulate('click');
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const afterChange = await changed();
|
||||
const afterChangeInput = afterChange.find('input').props().value;
|
||||
const afterClick = await clickButton();
|
||||
const afterProps = mockedComponent.find('Presentational').props();
|
||||
assert(
|
||||
beforeProps.messages.length === 0 &&
|
||||
afterChangeInput === testValue &&
|
||||
afterProps.messages.pop() === testValue &&
|
||||
afterClick.find('input').props().value === ''
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
`Presentational` 组件应渲染 Redux store 中的 `messages`。
|
||||
|
||||
```js
|
||||
async () => {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
const waitForIt = (fn) =>
|
||||
new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100));
|
||||
let beforeProps = mockedComponent.find('Presentational').props();
|
||||
const testValue = '__TEST__EVENT__INPUT__';
|
||||
const causeChange = (c, v) =>
|
||||
c.find('input').simulate('change', { target: { value: v } });
|
||||
const changed = () => {
|
||||
causeChange(mockedComponent, testValue);
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const clickButton = () => {
|
||||
mockedComponent.find('button').simulate('click');
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const afterChange = await changed();
|
||||
const afterChangeInput = afterChange.find('input').props().value;
|
||||
const afterClick = await clickButton();
|
||||
const afterProps = mockedComponent.find('Presentational').props();
|
||||
assert(
|
||||
beforeProps.messages.length === 0 &&
|
||||
afterChangeInput === testValue &&
|
||||
afterProps.messages.pop() === testValue &&
|
||||
afterClick.find('input').props().value === '' &&
|
||||
afterClick.find('ul').childAt(0).text() === testValue
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
## --after-user-code--
|
||||
|
||||
```jsx
|
||||
ReactDOM.render(<AppWrapper />, document.getElementById('root'))
|
||||
```
|
||||
|
||||
## --seed-contents--
|
||||
|
||||
```jsx
|
||||
// Redux:
|
||||
const ADD = 'ADD';
|
||||
|
||||
const addMessage = (message) => {
|
||||
return {
|
||||
type: ADD,
|
||||
message: message
|
||||
}
|
||||
};
|
||||
|
||||
const messageReducer = (state = [], action) => {
|
||||
switch (action.type) {
|
||||
case ADD:
|
||||
return [
|
||||
...state,
|
||||
action.message
|
||||
];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const store = Redux.createStore(messageReducer);
|
||||
|
||||
// React:
|
||||
const Provider = ReactRedux.Provider;
|
||||
const connect = ReactRedux.connect;
|
||||
|
||||
// Change code below this line
|
||||
class Presentational extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
input: '',
|
||||
messages: []
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.submitMessage = this.submitMessage.bind(this);
|
||||
}
|
||||
handleChange(event) {
|
||||
this.setState({
|
||||
input: event.target.value
|
||||
});
|
||||
}
|
||||
submitMessage() {
|
||||
this.setState((state) => ({
|
||||
input: '',
|
||||
messages: state.messages.concat(state.input)
|
||||
}));
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Type in a new Message:</h2>
|
||||
<input
|
||||
value={this.state.input}
|
||||
onChange={this.handleChange}/><br/>
|
||||
<button onClick={this.submitMessage}>Submit</button>
|
||||
<ul>
|
||||
{this.state.messages.map( (message, idx) => {
|
||||
return (
|
||||
<li key={idx}>{message}</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
// Change code above this line
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {messages: state}
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
submitNewMessage: (message) => {
|
||||
dispatch(addMessage(message))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Container = connect(mapStateToProps, mapDispatchToProps)(Presentational);
|
||||
|
||||
class AppWrapper extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Container/>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
# --solutions--
|
||||
|
||||
```jsx
|
||||
// Redux:
|
||||
const ADD = 'ADD';
|
||||
|
||||
const addMessage = (message) => {
|
||||
return {
|
||||
type: ADD,
|
||||
message: message
|
||||
}
|
||||
};
|
||||
|
||||
const messageReducer = (state = [], action) => {
|
||||
switch (action.type) {
|
||||
case ADD:
|
||||
return [
|
||||
...state,
|
||||
action.message
|
||||
];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const store = Redux.createStore(messageReducer);
|
||||
|
||||
// React:
|
||||
const Provider = ReactRedux.Provider;
|
||||
const connect = ReactRedux.connect;
|
||||
|
||||
// Change code below this line
|
||||
class Presentational extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
input: ''
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.submitMessage = this.submitMessage.bind(this);
|
||||
}
|
||||
handleChange(event) {
|
||||
this.setState({
|
||||
input: event.target.value
|
||||
});
|
||||
}
|
||||
submitMessage() {
|
||||
this.props.submitNewMessage(this.state.input);
|
||||
this.setState({
|
||||
input: ''
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Type in a new Message:</h2>
|
||||
<input
|
||||
value={this.state.input}
|
||||
onChange={this.handleChange}/><br/>
|
||||
<button onClick={this.submitMessage}>Submit</button>
|
||||
<ul>
|
||||
{this.props.messages.map( (message, idx) => {
|
||||
return (
|
||||
<li key={idx}>{message}</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
// Change code above this line
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {messages: state}
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
submitNewMessage: (message) => {
|
||||
dispatch(addMessage(message))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Container = connect(mapStateToProps, mapDispatchToProps)(Presentational);
|
||||
|
||||
class AppWrapper extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Container/>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
@ -0,0 +1,115 @@
|
||||
---
|
||||
id: 5a24c314108439a4d4036143
|
||||
title: 提取状态逻辑给 Redux
|
||||
challengeType: 6
|
||||
forumTopicId: 301429
|
||||
dashedName: extract-state-logic-to-redux
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
完成 React 组件后,我们需要把在本地 `state` 执行的逻辑移到 Redux 中, 这是为小规模 React 应用添加 Redux 的第一步。 该应用的唯一功能是把用户的新消息添加到无序列表中。 下面我们用简单的示例来演示 React 和 Redux 之间的配合。
|
||||
|
||||
# --instructions--
|
||||
|
||||
首先,定义 action 的类型 `ADD`,将其设置为常量 `ADD`。 接着,定义创建 action 的函数`addMessage()`,用该函数创建添加消息的 action, 把 `message` 传给创建 action 的函数并返回包含该消息的 `action`
|
||||
|
||||
接着,创建名为 `messageReducer()` 的 reducer 方法,为这些消息处理状态。 初始状态应为空数组。 reducer 向状态中的消息数组添加消息,或返回当前状态。 最后,创建 Redux store 并传给 reducer。
|
||||
|
||||
# --hints--
|
||||
|
||||
应存在一个值为字符串 `ADD` 的常量 `ADD`。
|
||||
|
||||
```js
|
||||
assert(ADD === 'ADD');
|
||||
```
|
||||
|
||||
创建 action 的函数 `addMessage` 应返回 `type` 等于 `ADD` 的对象,其返回的 `message` 即被传入的消息。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const addAction = addMessage('__TEST__MESSAGE__');
|
||||
return addAction.type === ADD && addAction.message === '__TEST__MESSAGE__';
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`messageReducer` 应是一个函数。
|
||||
|
||||
```js
|
||||
assert(typeof messageReducer === 'function');
|
||||
```
|
||||
|
||||
存在一个 store 且其初始状态为空数组。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const initialState = store.getState();
|
||||
return typeof store === 'object' && initialState.length === 0;
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
分发 `addMessage` 到 store 应添加新消息到状态中消息数组。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const initialState = store.getState();
|
||||
const isFrozen = DeepFreeze(initialState);
|
||||
store.dispatch(addMessage('__A__TEST__MESSAGE'));
|
||||
const addState = store.getState();
|
||||
return isFrozen && addState[0] === '__A__TEST__MESSAGE';
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`messageReducer` 被其它任何 actions 调用时应返回当前状态。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const addState = store.getState();
|
||||
store.dispatch({ type: 'FAKE_ACTION' });
|
||||
const testState = store.getState();
|
||||
return addState === testState;
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
## --seed-contents--
|
||||
|
||||
```jsx
|
||||
// Define ADD, addMessage(), messageReducer(), and store here:
|
||||
```
|
||||
|
||||
# --solutions--
|
||||
|
||||
```jsx
|
||||
const ADD = 'ADD';
|
||||
|
||||
const addMessage = (message) => {
|
||||
return {
|
||||
type: ADD,
|
||||
message
|
||||
}
|
||||
};
|
||||
|
||||
const messageReducer = (state = [], action) => {
|
||||
switch (action.type) {
|
||||
case ADD:
|
||||
return [
|
||||
...state,
|
||||
action.message
|
||||
];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const store = Redux.createStore(messageReducer);
|
||||
```
|
@ -0,0 +1,102 @@
|
||||
---
|
||||
id: 5a24c314108439a4d4036141
|
||||
title: React 和 Redux 入门
|
||||
challengeType: 6
|
||||
forumTopicId: 301430
|
||||
dashedName: getting-started-with-react-redux
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
这一系列挑战介绍的是 Redux 和 React 的配合, 我们先来回顾一下这两种技术的关键原则是什么。 React 是提供数据的视图库,能以高效、可预测的方式渲染视图。 Redux 是状态管理框架,可用于简化 APP 应用状态的管理。 在 React Redux app 应用中,通常可创建单一的 Redux store 来管理整个应用的状态。 React 组件仅订阅 store 中与其角色相关的数据, 可直接从 React 组件中分发 actions 以触发 store 对象的更新。
|
||||
|
||||
React 组件可以在本地管理自己的状态,但是对于复杂的应用来说,它的状态最好是用 Redux 保存在单一位置,有特定本地状态的独立组件例外。 当单个组件可能仅具有特定于其的本地状态时,算是例外。 最后一点是,Redux 没有内置的 React 支持,需要安装 `react-redux`包, 通过这个方式把 Redux 的 `state` 和 `dispatch` 作为 `props` 传给组件。
|
||||
|
||||
在接下来的挑战中,先要创建一个可输入新文本消息的 React 组件, 添加这些消息到数组里,在视图上显示数组。 这应该是 React 课程中的一个很好的回顾。 接着,创建 Redux store 和 actions 来管理消息数组的状态。 最后,使用 `react-redux` 连接 Redux store 和组件,从而将本地状态提取到 Redux store 中。
|
||||
|
||||
# --instructions--
|
||||
|
||||
从 `DisplayMessages` 组件开始。 把构造函数添加到此组件中,使用含两个属性的状态初始化该组件,这两个属性为:`input`(设置为空字符串),`messages`(设置为空数组)。
|
||||
|
||||
# --hints--
|
||||
|
||||
`DisplayMessages` 组件应渲染空的 `div` 元素。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages));
|
||||
return mockedComponent.find('div').text() === '';
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`DisplayMessages` 组件的构造函数应调用 `super`,传入 `props`。
|
||||
|
||||
```js
|
||||
(getUserInput) =>
|
||||
assert(
|
||||
(function () {
|
||||
const noWhiteSpace = __helpers.removeWhiteSpace(getUserInput('index'));
|
||||
return (
|
||||
noWhiteSpace.includes('constructor(props)') &&
|
||||
noWhiteSpace.includes('super(props')
|
||||
);
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`DisplayMessages` 组件的初始状态应是 `{input: "", messages: []}`。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages));
|
||||
const initialState = mockedComponent.state();
|
||||
return (
|
||||
typeof initialState === 'object' &&
|
||||
initialState.input === '' &&
|
||||
Array.isArray(initialState.messages) &&
|
||||
initialState.messages.length === 0
|
||||
);
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
## --after-user-code--
|
||||
|
||||
```jsx
|
||||
ReactDOM.render(<DisplayMessages />, document.getElementById('root'))
|
||||
```
|
||||
|
||||
## --seed-contents--
|
||||
|
||||
```jsx
|
||||
class DisplayMessages extends React.Component {
|
||||
// Change code below this line
|
||||
|
||||
// Change code above this line
|
||||
render() {
|
||||
return <div />
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
# --solutions--
|
||||
|
||||
```jsx
|
||||
class DisplayMessages extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
input: '',
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return <div/>
|
||||
}
|
||||
};
|
||||
```
|
@ -0,0 +1,260 @@
|
||||
---
|
||||
id: 5a24c314108439a4d4036142
|
||||
title: 首先在本地管理状态
|
||||
challengeType: 6
|
||||
forumTopicId: 301431
|
||||
dashedName: manage-state-locally-first
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
这一关的任务是完成 `DisplayMessages` 组件的创建。
|
||||
|
||||
# --instructions--
|
||||
|
||||
首先,在 `render()` 方法中,让组件渲染 `input`、`button`、`ul` 三个元素。 `input` 元素的改变会触发 `handleChange()` 方法。 此外,`input` 元素会渲染组件状态中 `input` 的值。 点击按钮 `button` 需触发 `submitMessage()` 方法。
|
||||
|
||||
接着,写出这两种方法。 `handleChange()` 方法会更新 `input` 为用户正在输入的内容。 `submitMessage()` 方法把当前存储在 `input` 的消息与本地状态的 `messages` 数组连接起来,并清除 `input` 的值。
|
||||
|
||||
最后,在 `ul` 中展示 `messages` 数组,其中每个元素内容需放到 `li` 元素内。
|
||||
|
||||
# --hints--
|
||||
|
||||
`DisplayMessages` 组件的初始状态应是 `{ input: "", messages: [] }`。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages));
|
||||
const initialState = mockedComponent.state();
|
||||
return (
|
||||
typeof initialState === 'object' &&
|
||||
initialState.input === '' &&
|
||||
initialState.messages.length === 0
|
||||
);
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`DisplayMessages` 组件应渲染含 `h2`、`button`、`ul`、`li` 四个子元素的`div`
|
||||
|
||||
```js
|
||||
async () => {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages));
|
||||
const waitForIt = (fn) =>
|
||||
new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100));
|
||||
const state = () => {
|
||||
mockedComponent.setState({ messages: ['__TEST__MESSAGE'] });
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const updated = await state();
|
||||
assert(
|
||||
updated.find('div').length === 1 &&
|
||||
updated.find('h2').length === 1 &&
|
||||
updated.find('button').length === 1 &&
|
||||
updated.find('ul').length === 1 &&
|
||||
updated.find('li').length > 0
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
`.map` 应该用于 `messages` 数组。
|
||||
|
||||
```js
|
||||
assert(code.match(/this\.state\.messages\.map/g));
|
||||
```
|
||||
|
||||
`input` 元素应渲染本地状态中的 `input` 值。
|
||||
|
||||
```js
|
||||
async () => {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages));
|
||||
const waitForIt = (fn) =>
|
||||
new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100));
|
||||
const causeChange = (c, v) =>
|
||||
c.find('input').simulate('change', { target: { value: v } });
|
||||
const testValue = '__TEST__EVENT__INPUT';
|
||||
const changed = () => {
|
||||
causeChange(mockedComponent, testValue);
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const updated = await changed();
|
||||
assert(updated.find('input').props().value === testValue);
|
||||
};
|
||||
```
|
||||
|
||||
调用 `handleChange` 方法时应更新状态中的 `input` 值为当前输入。
|
||||
|
||||
```js
|
||||
async () => {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages));
|
||||
const waitForIt = (fn) =>
|
||||
new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100));
|
||||
const causeChange = (c, v) =>
|
||||
c.find('input').simulate('change', { target: { value: v } });
|
||||
const initialState = mockedComponent.state();
|
||||
const testMessage = '__TEST__EVENT__MESSAGE__';
|
||||
const changed = () => {
|
||||
causeChange(mockedComponent, testMessage);
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const afterInput = await changed();
|
||||
assert(
|
||||
initialState.input === '' &&
|
||||
afterInput.state().input === '__TEST__EVENT__MESSAGE__'
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
单击 `Add message` 按钮应调用 `submitMessage` 方法,添加当前 `input` 到状态中的 `messages` 数组。
|
||||
|
||||
```js
|
||||
async () => {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages));
|
||||
const waitForIt = (fn) =>
|
||||
new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100));
|
||||
const causeChange = (c, v) =>
|
||||
c.find('input').simulate('change', { target: { value: v } });
|
||||
const initialState = mockedComponent.state();
|
||||
const testMessage_1 = '__FIRST__MESSAGE__';
|
||||
const firstChange = () => {
|
||||
causeChange(mockedComponent, testMessage_1);
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const firstResult = await firstChange();
|
||||
const firstSubmit = () => {
|
||||
mockedComponent.find('button').simulate('click');
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const afterSubmit_1 = await firstSubmit();
|
||||
const submitState_1 = afterSubmit_1.state();
|
||||
const testMessage_2 = '__SECOND__MESSAGE__';
|
||||
const secondChange = () => {
|
||||
causeChange(mockedComponent, testMessage_2);
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const secondResult = await secondChange();
|
||||
const secondSubmit = () => {
|
||||
mockedComponent.find('button').simulate('click');
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const afterSubmit_2 = await secondSubmit();
|
||||
const submitState_2 = afterSubmit_2.state();
|
||||
assert(
|
||||
initialState.messages.length === 0 &&
|
||||
submitState_1.messages.length === 1 &&
|
||||
submitState_2.messages.length === 2 &&
|
||||
submitState_2.messages[1] === testMessage_2
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
`submitMessage` 方法应清除当前输入。
|
||||
|
||||
```js
|
||||
async () => {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages));
|
||||
const waitForIt = (fn) =>
|
||||
new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100));
|
||||
const causeChange = (c, v) =>
|
||||
c.find('input').simulate('change', { target: { value: v } });
|
||||
const initialState = mockedComponent.state();
|
||||
const testMessage = '__FIRST__MESSAGE__';
|
||||
const firstChange = () => {
|
||||
causeChange(mockedComponent, testMessage);
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const firstResult = await firstChange();
|
||||
const firstState = firstResult.state();
|
||||
const firstSubmit = () => {
|
||||
mockedComponent.find('button').simulate('click');
|
||||
return waitForIt(() => mockedComponent);
|
||||
};
|
||||
const afterSubmit = await firstSubmit();
|
||||
const submitState = afterSubmit.state();
|
||||
assert(firstState.input === testMessage && submitState.input === '');
|
||||
};
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
## --after-user-code--
|
||||
|
||||
```jsx
|
||||
ReactDOM.render(<DisplayMessages />, document.getElementById('root'))
|
||||
```
|
||||
|
||||
## --seed-contents--
|
||||
|
||||
```jsx
|
||||
class DisplayMessages extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
input: '',
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
// Add handleChange() and submitMessage() methods here
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Type in a new Message:</h2>
|
||||
{ /* Render an input, button, and ul below this line */ }
|
||||
|
||||
{ /* Change code above this line */ }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
# --solutions--
|
||||
|
||||
```jsx
|
||||
class DisplayMessages extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
input: '',
|
||||
messages: []
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.submitMessage = this.submitMessage.bind(this);
|
||||
}
|
||||
handleChange(event) {
|
||||
this.setState({
|
||||
input: event.target.value
|
||||
});
|
||||
}
|
||||
submitMessage() {
|
||||
this.setState((state) => {
|
||||
const currentMessage = state.input;
|
||||
return {
|
||||
input: '',
|
||||
messages: state.messages.concat(currentMessage)
|
||||
};
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Type in a new Message:</h2>
|
||||
<input
|
||||
value={this.state.input}
|
||||
onChange={this.handleChange}/><br/>
|
||||
<button onClick={this.submitMessage}>Submit</button>
|
||||
<ul>
|
||||
{this.state.messages.map( (message, idx) => {
|
||||
return (
|
||||
<li key={idx}>{message}</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
@ -0,0 +1,107 @@
|
||||
---
|
||||
id: 5a24c314108439a4d4036146
|
||||
title: 映射 Dispatch 到 Props
|
||||
challengeType: 6
|
||||
forumTopicId: 301432
|
||||
dashedName: map-dispatch-to-props
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
`mapDispatchToProps()` 函数可为 React 组件提供特定的创建 action 的函数,以便组件可 dispatch actions,从而更改 Redux store 中的数据。 该函数的结构跟上一挑战中的`mapStateToProps()`函数相似, 它返回一个对象,把 dispatch actions 映射到属性名上,该属性名成为`props`。 然而,每个属性都返回一个用 action creator 及与 action 相关的所有数据调用 `dispatch` 的函数,而不是返回 `state` 的一部分。 可以访问 `dispatch`,因为在定义函数时,我们以参数形式把它传入 `mapDispatchToProps()` 了,这跟 `state` 传入 `mapStateToProps()` 是一样的。 在幕后,React Redux 用 Redux 的 `store.dispatch()` 来管理这些含 `mapDispatchToProps()` 的dispatches, 这跟它使用 `store.subscribe()` 来订阅映射到 `state` 的组件的方式类似。
|
||||
|
||||
例如,创建 action 的函数 `loginUser()` 把 `username` 作为 action payload, `mapDispatchToProps()` 返回给创建 action 的函数的对象如下:
|
||||
|
||||
```jsx
|
||||
{
|
||||
submitLoginUser: function(username) {
|
||||
dispatch(loginUser(username));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# --instructions--
|
||||
|
||||
编辑器上提供的是创建 action 的函数 `addMessage()`。 写出接收 `dispatch` 为参数的函数 `mapDispatchToProps()`,返回一个 dispatch 函数对象, 其属性为 `submitNewMessage`。该函数在 dispatch `addMessage()` 时为新消息提供一个参数。
|
||||
|
||||
# --hints--
|
||||
|
||||
`addMessage` 应返回含 `type` 和 `message` 两个键的对象。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const addMessageTest = addMessage();
|
||||
return (
|
||||
addMessageTest.hasOwnProperty('type') &&
|
||||
addMessageTest.hasOwnProperty('message')
|
||||
);
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`mapDispatchToProps` 应为函数。
|
||||
|
||||
```js
|
||||
assert(typeof mapDispatchToProps === 'function');
|
||||
```
|
||||
|
||||
`mapDispatchToProps` 应返回一个对象。
|
||||
|
||||
```js
|
||||
assert(typeof mapDispatchToProps() === 'object');
|
||||
```
|
||||
|
||||
从 `mapDispatchToProps` 通过 `submitNewMessage` 分发 `addMessage`,应向 dispatch 函数返回一条消息。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
let testAction;
|
||||
const dispatch = (fn) => {
|
||||
testAction = fn;
|
||||
};
|
||||
let dispatchFn = mapDispatchToProps(dispatch);
|
||||
dispatchFn.submitNewMessage('__TEST__MESSAGE__');
|
||||
return (
|
||||
testAction.type === 'ADD' && testAction.message === '__TEST__MESSAGE__'
|
||||
);
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
## --seed-contents--
|
||||
|
||||
```jsx
|
||||
const addMessage = (message) => {
|
||||
return {
|
||||
type: 'ADD',
|
||||
message: message
|
||||
}
|
||||
};
|
||||
|
||||
// Change code below this line
|
||||
```
|
||||
|
||||
# --solutions--
|
||||
|
||||
```jsx
|
||||
const addMessage = (message) => {
|
||||
return {
|
||||
type: 'ADD',
|
||||
message: message
|
||||
}
|
||||
};
|
||||
|
||||
// Change code below this line
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
submitNewMessage: function(message) {
|
||||
dispatch(addMessage(message));
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
@ -0,0 +1,69 @@
|
||||
---
|
||||
id: 5a24c314108439a4d4036145
|
||||
title: 映射 State 到 Props
|
||||
challengeType: 6
|
||||
forumTopicId: 301433
|
||||
dashedName: map-state-to-props
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
`Provider`可向 React 组件提供 `state` 和 `dispatch` ,但必须确切地指定所需要的 state 和 actions, 以确保每个组件只能访问所需的 state。 完成这个任务,需要创建两个函数:`mapStateToProps()`、`mapDispatchToProps()`。
|
||||
|
||||
在这两个函数中,声明 state 中函数所要访问的部分及需要 dispatch 的创建 action 的函数。 完成这些,我们就可以迎接下一个挑战,学习如何使用 React Redux 的 `connect` 方法来把函数连接到组件了。
|
||||
|
||||
**注意:** 在幕后,React Redux 用 `store.subscribe()` 方法来实现 `mapStateToProps()`。
|
||||
|
||||
# --instructions--
|
||||
|
||||
创建 `mapStateToProps()` 函数, 以 `state` 为参数,然后返回一个对象,该对象把 state 映射到特定属性名上, 这些属性能通过 `props` 访问组件。 由于此示例把 app 应用的整个状态保存在单一数组中,可把整个状态传给组件。 在返回的对象中创建 `messages` 属性,并设置为 `state`。
|
||||
|
||||
# --hints--
|
||||
|
||||
常量 `state` 应为空数组。
|
||||
|
||||
```js
|
||||
assert(Array.isArray(state) && state.length === 0);
|
||||
```
|
||||
|
||||
`mapStateToProps` 应为函数。
|
||||
|
||||
```js
|
||||
assert(typeof mapStateToProps === 'function');
|
||||
```
|
||||
|
||||
`mapStateToProps` 应返回一个对象。
|
||||
|
||||
```js
|
||||
assert(typeof mapStateToProps() === 'object');
|
||||
```
|
||||
|
||||
把 state 数组传入 `mapStateToProps` 后应返回赋值给 `messages` 键的数组。
|
||||
|
||||
```js
|
||||
assert(mapStateToProps(['messages']).messages.pop() === 'messages');
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
## --seed-contents--
|
||||
|
||||
```jsx
|
||||
const state = [];
|
||||
|
||||
// Change code below this line
|
||||
```
|
||||
|
||||
# --solutions--
|
||||
|
||||
```jsx
|
||||
const state = [];
|
||||
|
||||
// Change code below this line
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
messages: state
|
||||
}
|
||||
};
|
||||
```
|
@ -0,0 +1,69 @@
|
||||
---
|
||||
id: 5a24c314108439a4d403614a
|
||||
title: 从这里前进
|
||||
challengeType: 6
|
||||
forumTopicId: 301434
|
||||
dashedName: moving-forward-from-here
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
恭喜! 你完成了 React 和 Redux 的所有课程! 结束之前,还要再提一点。 通常,我们不会在这样的编辑器中编写 React 应用代码。 如果你在自己的计算机上使用 npm 和文件系统,这个挑战可让你一瞥 React 应用的语法之貌。 除了使用 `import` 语句(这些语句引入了各挑战中提供的所有依赖关系),其代码看起来类似。 “管理包(含 npm)”这一节更详细地介绍了 npm。
|
||||
|
||||
最后,写 React 和 Redux 的代码通常需要一些配置, 且很快会变得复杂起来。 如果你有兴趣在自己的电脑上尝试,<a href="https://github.com/facebookincubator/create-react-app" target="_blank" rel="nofollow">Create React App</a> 已配置好,并准备就绪。
|
||||
|
||||
或者,你可以在 CodePen 中启用 Babel 作为 JavaScript 预处理器,将 React 和 ReactDOM 添加为外部 JavaScript 资源,这样编写应用。
|
||||
|
||||
# --instructions--
|
||||
|
||||
在控制台输出消息 `'Now I know React and Redux!'`。
|
||||
|
||||
# --hints--
|
||||
|
||||
应该将 `Now I know React and Redux!` 这一消息应输出到控制台。
|
||||
|
||||
```js
|
||||
(getUserInput) =>
|
||||
assert(
|
||||
/console\s*\.\s*log\s*\(\s*('|"|`)Now I know React and Redux!\1\s*\)/.test(
|
||||
getUserInput('index')
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
## --seed-contents--
|
||||
|
||||
```jsx
|
||||
/*
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { Provider, connect } from 'react-redux'
|
||||
import { createStore, combineReducers, applyMiddleware } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
import rootReducer from './redux/reducers'
|
||||
import App from './components/App'
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(thunk)
|
||||
);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App/>
|
||||
</Provider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
*/
|
||||
|
||||
// Only change code below this line
|
||||
```
|
||||
|
||||
# --solutions--
|
||||
|
||||
```jsx
|
||||
console.log('Now I know React and Redux!');
|
||||
```
|
@ -0,0 +1,263 @@
|
||||
---
|
||||
id: 5a24c314108439a4d4036144
|
||||
title: 使用 Provider 连接 Redux 和 React
|
||||
challengeType: 6
|
||||
forumTopicId: 301435
|
||||
dashedName: use-provider-to-connect-redux-to-react
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
在上一挑战中,创建了 Redux store 和 action,分别用于处理消息数组和添加新消息。 下一步要为 React 提供访问 Redux store 及发起更新所需的 actions。 `react-redux` 包可帮助我们完成这些任务。
|
||||
|
||||
React Redux 提供的 API 有两个关键的功能:`Provider` 和 `connect`。 会在另一个挑战会介绍 `connect`。 `Provider`是 React Redux 包装 React 应用的 wrapper 组件, 它允许访问整个组件树中的 Redux `store` 及 `dispatch`(分发)方法。 `Provider` 需要两个 props:Redux store 和 App 应用的子组件。 用于 App 组件的 `Provider` 可这样定义:
|
||||
|
||||
```jsx
|
||||
<Provider store={store}>
|
||||
<App/>
|
||||
</Provider>
|
||||
```
|
||||
|
||||
# --instructions--
|
||||
|
||||
此时,编辑器上显示的是过去几个挑战中所有代码, 包括 Redux store、actions、`DisplayMessages` 组件。 新出现的代码是底部的`AppWrapper`组件, 这个顶级组件可用于渲染 `ReactRedux` 的 `Provider`,并把 Redux 的 store 作为 props 传入。 接着,渲染 `DisplayMessages` 为子组件。 完成这些任务后,会看到 React 组件渲染到页面上。
|
||||
|
||||
**注意:** React Redux 在此可作全局变量,因此可通过点号表示法访问 Provider。 利用这一点,编辑器上的代码把 `Provider` 设置为常量,便于你在 `AppWrapper` 渲染方法中使用。
|
||||
|
||||
# --hints--
|
||||
|
||||
`AppWrapper` 应渲染。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
return mockedComponent.find('AppWrapper').length === 1;
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`Provider` 组件应传入相当于 Redux store 的 `store` 参数。
|
||||
|
||||
```js
|
||||
(getUserInput) =>
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
return __helpers
|
||||
.removeWhiteSpace(getUserInput('index'))
|
||||
.includes('<Providerstore={store}>');
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`DisplayMessages` 应渲染为 `AppWrapper` 的子组件。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
return (
|
||||
mockedComponent.find('AppWrapper').find('DisplayMessages').length === 1
|
||||
);
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
`DisplayMessages` 组件应渲染 `h2`、`input`、`button`、`ul` 四个元素。
|
||||
|
||||
```js
|
||||
assert(
|
||||
(function () {
|
||||
const mockedComponent = Enzyme.mount(React.createElement(AppWrapper));
|
||||
return (
|
||||
mockedComponent.find('div').length === 1 &&
|
||||
mockedComponent.find('h2').length === 1 &&
|
||||
mockedComponent.find('button').length === 1 &&
|
||||
mockedComponent.find('ul').length === 1
|
||||
);
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
## --after-user-code--
|
||||
|
||||
```jsx
|
||||
ReactDOM.render(<AppWrapper />, document.getElementById('root'))
|
||||
```
|
||||
|
||||
## --seed-contents--
|
||||
|
||||
```jsx
|
||||
// Redux:
|
||||
const ADD = 'ADD';
|
||||
|
||||
const addMessage = (message) => {
|
||||
return {
|
||||
type: ADD,
|
||||
message
|
||||
}
|
||||
};
|
||||
|
||||
const messageReducer = (state = [], action) => {
|
||||
switch (action.type) {
|
||||
case ADD:
|
||||
return [
|
||||
...state,
|
||||
action.message
|
||||
];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const store = Redux.createStore(messageReducer);
|
||||
|
||||
// React:
|
||||
|
||||
class DisplayMessages extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
input: '',
|
||||
messages: []
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.submitMessage = this.submitMessage.bind(this);
|
||||
}
|
||||
handleChange(event) {
|
||||
this.setState({
|
||||
input: event.target.value
|
||||
});
|
||||
}
|
||||
submitMessage() {
|
||||
this.setState((state) => {
|
||||
const currentMessage = state.input;
|
||||
return {
|
||||
input: '',
|
||||
messages: state.messages.concat(currentMessage)
|
||||
};
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Type in a new Message:</h2>
|
||||
<input
|
||||
value={this.state.input}
|
||||
onChange={this.handleChange}/><br/>
|
||||
<button onClick={this.submitMessage}>Submit</button>
|
||||
<ul>
|
||||
{this.state.messages.map( (message, idx) => {
|
||||
return (
|
||||
<li key={idx}>{message}</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Provider = ReactRedux.Provider;
|
||||
|
||||
class AppWrapper extends React.Component {
|
||||
// Render the Provider below this line
|
||||
|
||||
// Change code above this line
|
||||
};
|
||||
```
|
||||
|
||||
# --solutions--
|
||||
|
||||
```jsx
|
||||
// Redux:
|
||||
const ADD = 'ADD';
|
||||
|
||||
const addMessage = (message) => {
|
||||
return {
|
||||
type: ADD,
|
||||
message
|
||||
}
|
||||
};
|
||||
|
||||
const messageReducer = (state = [], action) => {
|
||||
switch (action.type) {
|
||||
case ADD:
|
||||
return [
|
||||
...state,
|
||||
action.message
|
||||
];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const store = Redux.createStore(messageReducer);
|
||||
|
||||
// React:
|
||||
|
||||
class DisplayMessages extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
input: '',
|
||||
messages: []
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.submitMessage = this.submitMessage.bind(this);
|
||||
}
|
||||
handleChange(event) {
|
||||
this.setState({
|
||||
input: event.target.value
|
||||
});
|
||||
}
|
||||
submitMessage() {
|
||||
this.setState((state) => {
|
||||
const currentMessage = state.input;
|
||||
return {
|
||||
input: '',
|
||||
messages: state.messages.concat(currentMessage)
|
||||
};
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Type in a new Message:</h2>
|
||||
<input
|
||||
value={this.state.input}
|
||||
onChange={this.handleChange}/><br/>
|
||||
<button onClick={this.submitMessage}>Submit</button>
|
||||
<ul>
|
||||
{this.state.messages.map( (message, idx) => {
|
||||
return (
|
||||
<li key={idx}>{message}</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Provider = ReactRedux.Provider;
|
||||
|
||||
class AppWrapper extends React.Component {
|
||||
// Change code below this line
|
||||
render() {
|
||||
return (
|
||||
<Provider store = {store}>
|
||||
<DisplayMessages/>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
// Change code above this line
|
||||
};
|
||||
```
|
Reference in New Issue
Block a user