mirror of
https://github.com/microsoft/frontend-bootcamp.git
synced 2026-01-26 14:56:42 +08:00
Rewrite of Day 1 to use modern React (#294)
* update to hooks * more class to function * cleanup * finish ts final * update html lesson * add lessons page * clean up * move getters into context * adding type * fix bug * step 5 cleanup * init final pass * text tweak * fix ternaries * readme cleanup * fixed root readme
This commit is contained in:
@@ -2,71 +2,72 @@
|
||||
|
||||
In React, the data travels in one direction: top-down in the form of state propagating down the component hierarchy. Only the component containing the state can change the state itself. When a UI interaction occurs, a stateful component must pass down an event handler to the UI component triggering the event in order to signal a state change.
|
||||
|
||||
[Step #3 of "Thinking in React"](https://reactjs.org/docs/thinking-in-react.html) suggests finding the "minimal set of mutable state" that your application requires. So in this demo we are going to add that "minimal state" to our application and drive our UI off of that data. With that done, the next step will be to create ways to modify that state, which will in turn cascade down through our UI. This [reconciliation](https://reactjs.org/docs/reconciliation.html) process, figuring out what in your UI needs to change based on changing state, is what React excels at.
|
||||
[Step #3 of "Thinking in React"](https://reactjs.org/docs/thinking-in-react.html#step-3-identify-the-minimal-but-complete-representation-of-ui-state) suggests finding the "minimal set of mutable state" that your application requires. What pieces of state can we identify?
|
||||
|
||||
[Step #4 of "Thinking in React"](https://reactjs.org/docs/thinking-in-react.html#step-4-identify-where-your-state-should-live) asks us to think about where our state should live.
|
||||
|
||||
- Is the state local to a single component?
|
||||
- Is the state derived from another state?
|
||||
- Is the state primarily in one component but shared with others?
|
||||
- Is the state global?
|
||||
|
||||
## Adding state to TodoApp
|
||||
|
||||
Inside our `TodoApp` class, we will add the minimal state for our application, which includes just two keys: `todos` and `filter`. We don't need to worry about a `remaining` count because it can be calculated by counting the number of todos where the `completed` field is set to `false`.
|
||||
|
||||
So here is our full constructor:
|
||||
Inside of our `TodoApp` component we only need to track two pieces of state, our `todos` and the current `filter`. We don't need to worry about a `remaining` count because it can be calculated by counting the number of todos where `status` is set to `active`.
|
||||
|
||||
```jsx
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
todos: {
|
||||
'04': {
|
||||
label: 'Todo 4',
|
||||
completed: true
|
||||
},
|
||||
'03': {
|
||||
label: 'Todo 3',
|
||||
completed: false
|
||||
},
|
||||
'02': {
|
||||
label: 'Todo 2',
|
||||
completed: false
|
||||
},
|
||||
'01': {
|
||||
label: 'Todo 1',
|
||||
completed: false
|
||||
}
|
||||
export const TodoApp = () => {
|
||||
const [filter, setFilter] = React.useState<FilterTypes>('all');
|
||||
const [todos, setTodos] = React.useState<Todos>([
|
||||
{
|
||||
id: '04',
|
||||
label: 'Todo 4',
|
||||
status: 'completed',
|
||||
},
|
||||
filter: 'active'
|
||||
};
|
||||
}
|
||||
```
|
||||
{
|
||||
id: '03',
|
||||
label: 'Todo 3',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
label: 'Todo 2',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '01',
|
||||
label: 'Todo 1',
|
||||
status: 'active',
|
||||
},
|
||||
]);
|
||||
|
||||
> You could also use an array to represent your todos. Array manipulation can be easier in some cases, but this object approach simplifies other functionality and will ultimately be more performant.
|
||||
```
|
||||
|
||||
## Passing state through to UI
|
||||
|
||||
Now we can pass `filter` and `todos` into our components.
|
||||
|
||||
```jsx
|
||||
render() {
|
||||
const { filter, todos } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader filter={filter} />
|
||||
<TodoList todos={todos} filter={filter} />
|
||||
<TodoFooter todos={todos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader filter={filter} />
|
||||
<TodoList todos={todos} filter={filter} />
|
||||
<TodoFooter todos={todos} />
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
## State-driven TodoList
|
||||
|
||||
I've already pulled out our props into `filter` and `todos` variables, and written a bit of JS that will return an array of filtered todo `id`s. We'll be using that filtered array to render our todo items.
|
||||
I've already pulled out our props into `filter` and `todos` variables, and written a bit of JS that will return an array of filtered todos. We'll be using that filtered array to render our todo items.
|
||||
|
||||
> `todos[id]` returns the todo matching the `id` passed in, and the spread operator (...) is the same as saying `label={todos[id].label} completed={todos[id].completed}`
|
||||
React requires any dynamic length list to have unique `key` properties, for which we can use the `todo.id`. This key helps React to only re-render the parts of the list that changes.
|
||||
|
||||
```jsx
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map(id => (
|
||||
<TodoListItem key={id} id={id} {...todos[id]} />
|
||||
{filteredTodos.map((todo) => (
|
||||
<TodoListItem key={todo.id} {...todo} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
@@ -74,7 +75,7 @@ return (
|
||||
|
||||
## State-driven and stateful TodoHeader
|
||||
|
||||
Within the header, we've got a situation where we not only want to pass `filter` state down to it, but we also want to maintain state within the control. Fortunately, this is no problem at all for React. First off let's deal with the incoming state.
|
||||
In `TodoHeader.tsx` we are going to both display the selected filter state, and track the text for a new todo.
|
||||
|
||||
### Conditional class names
|
||||
|
||||
@@ -82,21 +83,21 @@ In CSS-based styling, visual states are applied by adding and removing classes.
|
||||
|
||||
```jsx
|
||||
<nav className="filter">
|
||||
<button className={filter === 'all' ? 'selected' : ''}>all</button>
|
||||
<button className={filter === 'active' ? 'selected' : ''}>active</button>
|
||||
<button className={filter === 'completed' ? 'selected' : ''}>completed</button>
|
||||
<button className={filter === 'all' ? 'selected' : ''}> all</button>
|
||||
<button className={filter === 'active' ? 'selected' : ''}> active</button>
|
||||
<button className={filter === 'completed' ? 'selected' : ''}> completed</button>
|
||||
</nav>
|
||||
```
|
||||
|
||||
> The [ternary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) `condition ? expressionIfTrue : expressionIfFalse` is widely used in React code, as each expression could be a string for a className or even a JSX element.
|
||||
> The [ternary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) `condition ? ifTrue : ifFalse` is often used to conditionally render a string or JSX element. In the case that the condition is false the `className` is simply ommited.
|
||||
|
||||
### Adding a controlled input
|
||||
|
||||
In React, form elements such as `<input>`, `<textarea>`, and `<select>` can be used as either **uncontrolled** or **controlled**.
|
||||
|
||||
An **uncontrolled input** maintains its current value internally and updates it based on user interactions (entering text, choosing options, etc). The code only pulls the value from the input when it's needed, such as on submit. This is similar to how inputs in a plain HTML form work.
|
||||
An **uncontrolled input** maintains its current value internally and updates that value based on user interactions (entering text, choosing options, etc). Our code only polls the value from the input when it's needed, such as on submit. This is similar to how inputs in a plain HTML form work.
|
||||
|
||||
A **controlled input** takes its current value from a prop and use a callback to notify the parent component of changes made by the user. The input's value doesn't change until the parent component updates the input's props in response to the callback.
|
||||
A **controlled input** takes its current value from a prop or state and uses a callback to modify that value when a change is made by the user. This is usually the prefered method when writing React.
|
||||
|
||||
> The distinction between controlled and uncontrolled is important to understand when writing or using form components, and misunderstandings of this concept are a very common source of bugs. See [this article](https://goshakkk.name/controlled-vs-uncontrolled-inputs-react/) for a more detailed explanation.
|
||||
|
||||
@@ -105,21 +106,21 @@ Let's try changing the text field in our `TodoHeader` component to a controlled
|
||||
1. A state variable to hold the input's value:
|
||||
|
||||
```jsx
|
||||
this.state = { labelInput: '' };
|
||||
const [inputText, setInputText] = React.useState('');
|
||||
```
|
||||
|
||||
2. A callback function to update that value:
|
||||
|
||||
```jsx
|
||||
_onChange = evt => {
|
||||
this.setState({ labelInput: evt.target.value });
|
||||
const onInput = (e) => {
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
```
|
||||
|
||||
With those two pieces in place, we can update our uncontrolled input to being controlled.
|
||||
|
||||
```jsx
|
||||
<input value={this.state.labelInput} onChange={this._onChange} className="textfield" placeholder="add todo" />
|
||||
<input value={inputText} onChange={onInput} className="textfield" placeholder="add todo" />
|
||||
```
|
||||
|
||||
> If you have React Dev Tools installed, open them up and take a look at `labelInput` as we type in the input.
|
||||
|
||||
Reference in New Issue
Block a user