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