Link document:
React in w3school: https://www.w3schools.com/react/default.asp
redux: https://redux.js.org/tutorials/fundamentals/part-1-overview
demo: https://github.com/lbthucsgt/react-redux-tutorial
Class component and function component
# class component
class Car extends React.Component {
render() {
return <h2>Hi, I am a Car!</h2>;
}
}
# function component
function Car() {
return <h2>Hi, I am a Car!</h2>;
}
Từ React version 16.8, sử dụng hook thay vì class component. Nên bài viết này tập trung vào function component.
React Props
link test: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_props_pass2
function Car(props) {
return <h2>I am a { props.brand }!</h2>;
}
function Garage() {
const carName = "Ford";
return (
<>
<h1>Who lives in my garage?</h1>
<Car brand={ carName } />
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Garage />);
React Event
link test: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_events_handler
function Football() {
const shoot = () => {
alert("Great Shot!");
}
return (
<button onClick={shoot}>Take the shot!</button>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Football />);
# passing argument
function Football() {
const shoot = (a) => {
alert(a);
}
return (
<button onClick={() => shoot("Goal!")}>Take the shot!</button>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Football />);
link test: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_events_event
# passing event
function Football() {
const shoot = (a, b) => {
alert(b.type);
/*
'b' represents the React event that triggered the function,
in this case the 'click' event
*/
}
return (
<button onClick={(event) => shoot("Goal!", event)}>Take the shot!</button>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Football />);
React Lists
link test: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_list1
function Car(props) {
return <li>I am a { props.brand }</li>;
}
function Garage() {
const cars = ['Ford', 'BMW', 'Audi'];
return (
<>
<h1>Who lives in my garage?</h1>
<ul>
{cars.map((car) => <Car brand={car} />)}
</ul>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Garage />);
link: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_lists_keys
# Keys
Keys allow React to keep track of elements. This way, if an item is updated or removed,
only that item will be re-rendered instead of the entire list.
Keys need to be unique to each sibling. But they can be duplicated globally.
function Car(props) {
return <li>I am a { props.brand }</li>;
}
function Garage() {
const cars = [
{id: 1, brand: 'Ford'},
{id: 2, brand: 'BMW'},
{id: 3, brand: 'Audi'}
];
return (
<>
<h1>Who lives in my garage?</h1>
<ul>
{cars.map((car) => <Car key={car.id} brand={car.brand} />)}
</ul>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Garage />);
React Form
Document: https://www.w3schools.com/react/react_forms.asp
Form will work as normal, the form will submit and the page will refresh.
But this is generally not what we want to happen in React.
We want to prevent this default behavior and let React control the form.
link: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_forms_submit
import { useState } from 'react';
import ReactDOM from 'react-dom/client';
function MyForm() {
const [name, setName] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
alert(`The name you entered was: ${name}`)
}
return (
<form onSubmit={handleSubmit}>
<label>Enter your name:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<input type="submit" />
</form>
)
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<MyForm />);
React Hook
Hooks were added to React in version 16.8.
Hooks allow function components to have access to state and other React features.
Because of this, class components are generally no longer needed.
You must import Hooks from react.
There are 3 rules for hooks:
Hooks can only be called inside React function components.
Hooks can only be called at the top level of a component.
Hooks cannot be conditional
useState Hook
The React useState Hook allows us to track state in a function component.
State generally refers to data or properties that need to be tracking in an application.
link: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_usestate_update
# Update State
import { useState } from "react";
import ReactDOM from "react-dom/client";
function FavoriteColor() {
const [color, setColor] = useState("red");
return (
<>
<h1>My favorite color is {color}!</h1>
<button
type="button"
onClick={() => setColor("blue")}
>Blue</button>
</>
)
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<FavoriteColor />);
link test: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_usestate4
import { useState } from "react";
import ReactDOM from "react-dom/client";
function Car() {
const [car, setCar] = useState({
brand: "Ford",
model: "Mustang",
year: "1964",
color: "red"
});
const updateColor = () => {
setCar(previousState => {
return { ...previousState, color: "blue" }
});
}
return (
<>
<h1>My {car.brand}</h1>
<p>
It is a {car.color} {car.model} from {car.year}.
</p>
<button
type="button"
onClick={updateColor}
>Blue</button>
</>
)
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Car />);
useEffect Hooks
- useEffect Hook cho phép bạn gọi side effect(phản ứng phụ) trong componenet.
- ví dụ về side effect là: fetching data, directly updating the DOM, and timers
- cú pháp: useEffect(<function>, <dependency>)
link test: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_useeffect_settimeout
import { useState, useEffect } from "react";
import ReactDOM from "react-dom/client";
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
setCount((count) => count + 1);
}, 1000);
});
return <h1>I've rendered {count} times!</h1>;
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Timer />);
- ví dụ này count render liên tục thay vì render một lần
- đó là vì useEffect runs on every render. Khi count changes, a render happens, which then triggers another effect.
- để tránh như vậy cần tham khảo cách phía dưới
useEffect(() => {
// Runs on every render
});
useEffect(() => {
// Runs only on the first render
}, []);
useEffect(() => {
// Runs on the first render
// And any time any dependency value changes
}, [prop, state]);
link test: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_useeffect_settimeout3
import { useState, useEffect } from "react";
import ReactDOM from "react-dom/client";
function Counter() {
const [count, setCount] = useState(0);
const [calculation, setCalculation] = useState(0);
useEffect(() => {
setCalculation(() => count * 2);
}, [count]); // <- add the count variable here. Khi count thay đổi thì hàm useEffect được gọi lại
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>+</button>
<p>Calculation: {calculation}</p>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Counter />);
link test: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_customhook1
import { useState, useEffect } from "react";
import ReactDOM from "react-dom/client";
const Home = () => {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/todos")
.then((res) => res.json())
.then((data) => setData(data));
}, []);
return (
<>
{data &&
data.map((item) => {
return <p key={item.id}>{item.title}</p>;
})}
</>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Home />);
useReducer
The useReducer Hook is similar to the useState Hook.
It allows for custom state logic.
If you find yourself keeping track of multiple pieces of state that rely on complex logic, useReducer may be useful
link test: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_usereducer
import React from 'react'
import { useReducer } from "react";
const initialTodos = [
{
id: 1,
title: "Todo 1",
complete: false,
},
{
id: 2,
title: "Todo 2",
complete: false,
},
];
const reducer = (state, action) => {
switch (action.type) {
case "COMPLETE":
return state.map((todo) => {
if (todo.id === action.id) {
return { ...todo, complete: !todo.complete };
} else {
return todo;
}
});
default:
return state;
}
};
const Test = (props) => {
const [todos, dispatch] = useReducer(reducer, initialTodos);
const handleComplete = (todo) => {
dispatch({ type: "COMPLETE", id: todo.id });
};
return (
<div>
<div className='scroll-up-dashboard'>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/test">Test</a></li>
</ul>
</div>
{todos.map((todo) => (
<div key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.complete}
onChange={() => handleComplete(todo)}
/>
{todo.title} complete: {todo.complete ? 'true' : 'false'}
</label>
</div>
))}
</div>
)
};
export default Test
useCallback Hook
The useCallback Hook chỉ chạy khi một trong các component con của nó được updated. Điều này có thể tăng performence.
useCallback is to prevent a component from re-rendering unless its props have changed
The useCallback and useMemo Hooks là giống nhau. Khác biệt là useMemo returns a memoized value còn useCallback returns a memoized function.
referential equality => Every time a component re-renders, its functions get recreated
(Khi một trong các component bị thay đổi, các compnent khác liên quan tới ở function cũng bị thay đổi theo)
Vì vậy nếu thấy component cùng cấp bị thay đổi thì dùng useCallback để tránh bị ảnh hưởng hiệu suất.
link test: https://www.w3schools.com/react/showreact.asp?filename=demo2_react_usecallback2
# ở ví dụ dưới đây, useCallback hook sẽ giúp ngăn chặn component re-render không cần thiết
# bằng cách bỏ hàm setDodos vào trong useCallback function.
# index.js
import { useState, useCallback } from "react";
import ReactDOM from "react-dom/client";
import Todos from "./Todos";
const App = () => {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const increment = () => {
setCount((c) => c + 1);
};
// original: sẽ render lại Todos khi button được click
/* const addTodo = () => {
setTodos((t) => [...t, "New Todo"]);
};*/
const addTodo = useCallback(() => { // giúp ngăn component Todos render lại không cần thiết
setTodos((t) => [...t, "New Todo"]);
}, [todos]);
return (
<>
<Todos todos={todos} addTodo={addTodo} />
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
</div>
</>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
# Todos.js
import { memo } from "react";
const Todos = ({ todos, addTodo }) => { // truyền cả biến và hàm xuống component con
console.log("child render");
return (
<>
<h2>My Todos</h2>
{todos.map((todo, index) => {
return <p key={index}>{todo}</p>;
})}
<button onClick={addTodo}>Add Todo</button> // ở đây sự kiện được truyền xuống của parent component
</>
);
};
export default memo(Todos);
React Memo
Dùng memo có thể giữ cho React không re-render component nếu props của nó không bị thay đổi.
Ví dụ sau có thể trùng lặp với ví dụ trên.
# index.js
import { useState } from "react";
import ReactDOM from "react-dom/client";
import Todos from "./Todos";
const App = () => {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState(["todo 1", "todo 2"]);
const increment = () => {
setCount((c) => c + 1);
};
return (
<>
<Todos todos={todos} />
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
</div>
</>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
# Todos.js
import { memo } from "react";
const Todos = ({ todos }) => {
console.log("child render");
return (
<>
<h2>My Todos</h2>
{todos.map((todo, index) => {
return <p key={index}>{todo}</p>;
})}
</>
);
};
export default memo(Todos);
Custom Hook:
link document: https://www.w3schools.com/react/react_customhooks.asp
<img class="img_ev3q" src="https://redux.js.org/assets/images/one-way-data-flow-04fe46332c1ccb3497ecb04b94e55b97.png" alt="One-way data flow" width="800" height="541" loading="lazy">
redux
Redux helps you manage "global" state - state that is needed across many parts of your application.
Redux is more useful when:
- You have large amounts of application state that are needed in many places in the app
- The app state is updated frequently over time
- The logic to update that state may be complex
- The app has a medium or large-sized codebase, and might be worked on by many people
The center of every Redux application is the store. A "store" is a container that holds your application's global state.
<img class="img_ev3q" src="https://d33wubrfki0l68.cloudfront.net/01cc198232551a7e180f4e9e327b5ab22d9d14e7/b33f4/assets/images/reduxdataflowdiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif" alt="Redux data flow diagram" width="800" height="600" loading="lazy">
Redux Store
Holds the current application state inside
Allows access to the current state via store.getState();
Allows state to be updated via store.dispatch(action);
Registers listener callbacks via store.subscribe(listener); // không dùng nhiều, vì useSelector đã hỗ trợ
Handles unregistering of listeners via the unsubscribe function returned by store.subscribe(listener). // không dùng nhiều, vì useSelector đã hỗ trợ
React-Redux hook: useSelector
- which lets your React components read data from the Redux store.
- useSelector automatically subscribes to the Redux store for us
- All we have to do is call useSelector() once in our component, and it does the rest of the work for us
- Lưu ý: useSelector sử dụng strict ===, nghĩa là component sẽ re-render mỗi khi state có tạo reference mới
vd: return state.todos.map(todo => todo.text) // tạo reference mới
# dùng useSelector để lấy state ra khỏi global state
import { useSelector } from 'react-redux'
const setTodo = state => state.todos;
const todos = useSelector(setTodo) // lấy state "todos" ra khỏi state global
const todos = useSelector(state => state.todos) // cách khác để lấy state "todos"
# hàm cơ bản
store.getState(): lấy global state ra
# useDispatch hook: dispatch một action
You can think of dispatching actions as "triggering an event" in the application.
import {useDispatch} from 'react-redux'
const dispatch = useDispatch();
dispatch({type:COUNTER_DECREMENT}) // dùng dispatch function để dispatch
store.dispatch({ type: 'counter/incremented' }) // dùng store để dispatch
# action
action là một object, ví dụ: { type: 'counter/incremented', payload: 10 }
# reducer: thay đổi trạng thái state (ở global)
const initialState = {value : 0}
function counterReducer(state = initialState, action) {
const { type } = action
switch (type) {
case 'counter/incremented':
return {
...state,
value: state.value + 1,
}
case COUNTER_DECREMENT:
return {
...state,
value: state.value - 1,
}
default:
return state
}
}
export default counterReducer
Hầu hết Form không dùng redux để lưu trữ state.
Thay vào đó, giữ data ở Form component bằng local state. Sau đó dispatch Redux action để update global state khi user thực hiện xong công việc.
Sau khi update global state, view sẽ được render lại.
# có thể dùng shallowEqual để chặn các component con re-render khi một trong các component ngang hàng thay đổi
# hoặc có thể dùng memo để bao component con như bên react
# ví dụ sau dùng shallowEqual
import React from 'react'
import { useSelector, shallowEqual } from 'react-redux'
import TodoListItem from './TodoListItem'
const selectTodoIds = state => state.todos.map(todo => todo.id)
const TodoList = () => {
const todoIds = useSelector(selectTodoIds, shallowEqual)
const renderedListItems = todoIds.map(todoId => {
return <TodoListItem key={todoId} id={todoId} />
})
return <ul className="todo-list">{renderedListItems}</ul>
}
Example to use react redux:
# action/type.js
export const COUNTER_INCREMENT = "COUNTER_INCREMENT"
export const COUNTER_DECREMENT = "COUNTER_DECREMENT"
# reducer/counter.js
import { COUNTER_INCREMENT, COUNTER_DECREMENT } from '../actions/type'
const initialState = {value : 0}
function counterReducer(state = initialState, action) {
const { type } = action
switch (type) {
case COUNTER_INCREMENT:
return {
...state,
value: state.value + 1,
}
case COUNTER_DECREMENT:
return {
...state,
value: state.value - 1,
}
default:
return state
}
}
export default counterReducer
# test/counter.js
import React from 'react'
import store from "../store";
import {COUNTER_INCREMENT, COUNTER_DECREMENT} from "../actions/type";
import {useDispatch, useSelector} from 'react-redux'
const Test = (props) => {
const dispatch = useDispatch();
const value = useSelector(state => state.counterReducer.value);
return (
<div>
You clicked: {value} times
<button onClick={() => store.dispatch({type:COUNTER_INCREMENT})}>+</button>
<button onClick={() => dispatch({type:COUNTER_DECREMENT})}>-</button> // another way to dispatch
</div>
)
};
export default Test
<img class="img_ev3q" src="https://redux.js.org/assets/images/ReduxAsyncDataFlowDiagram-d97ff38a0f4da0f327163170ccc13e80.gif" alt="Redux async data flow diagram" width="800" height="600" loading="lazy">
use thunk
npm install redux-thunk
# src/store.js
import { legacy_createStore as createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from "redux-devtools-extension"
import thunk from 'redux-thunk'
import rootReducer from './reducers'
const initialState = {}
const middleware = [thunk]
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
)
export default store
// Thunk function
export async function fetchTodos(dispatch, getState) {
const response = await client.get('/fakeApi/todos')
dispatch({ type: 'todos/todosLoaded', payload: response.todos })
}
// Write a synchronous outer function that receives the `text` parameter:
export function saveNewTodo(text) {
// And then creates and returns the async thunk function:
return async function saveNewTodoThunk(dispatch, getState) {
// ✅ Now we can use the text value and send it to the server
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
dispatch({ type: 'todos/todoAdded', payload: response.todo })
}
}
# usage
import { fetchTodos } from './features/todos/todosSlice'
store.dispatch(fetchTodos)
if (e.which === 13 && trimmedText) {
// Create the thunk function with the text the user wrote
const saveNewTodoThunk = saveNewTodo(trimmedText)
// Then dispatch the thunk function itself
dispatch(saveNewTodoThunk)
another example
# thunk here
export const updateScrollUp = (data) => async (dispatch) => {
try {
const res = await ScrollUpService.update(data) // make api call
dispatch({
type: UPDATE_SCROLL_UP,
payload: data,
})
return Promise.resolve(res.data)
} catch (error) {
return Promise.reject(error)
}
}
// call here
dispatch(updateScrollUp(data))
.then(response => {
LogService.debug('updateSetting.response', response)
showSuccessMessage(CommonService.messages.settingUpdated)
})
.catch(e => {
handleApiError(e)
})
.finally(() => {
setUpdatingSetting(false)
})