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:
Shaun Hamilton
2021-08-14 03:57:13 +01:00
committed by GitHub
parent 4df2a0c542
commit c2a11ad00d
1215 changed files with 790 additions and 449 deletions

View File

@ -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);
```

View File

@ -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>
);
}
};
```

View File

@ -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>
);
}
};
```

View File

@ -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);
```

View File

@ -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/>
}
};
```

View File

@ -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>
);
}
};
```

View File

@ -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));
}
}
};
```

View File

@ -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
}
};
```

View File

@ -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!');
```

View File

@ -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` 需要兩個 propsRedux 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
};
```