Damien CHAZOULE
Developer
FullStack
dmnchzl@pm.me
www.dmnchzl.dev
damien-chazoule
@dmn_chzl
www.dev.to/dmnchzl
www.github.com/dmnchzl
dmnchzl.medium.com
www.gitlab.com/dmnchzl
Données partagées entre plusieurs composants / niveaux de composants
Meilleure scalabilité / Mise à l'échelle de l'application
Pattern de développement / Flux de données
Réusabilité et meilleure maintenabilité
Paradigme de programmation réactive
Simplicité d'implémentation !== Simplicité d'utilisation
Component
State
State Management
State Management
S
T
O
R
E
Dispatch
npm install react-redux redux redux-thunk
/
├── public/
├── src/
│ ├── store/
│ │ ├── pizzas/
│ │ │ ├── actions.js
│ │ │ └── reducer.js
│ │ └── index.js
│ ├── App.jsx
│ ├── index.css
│ ├── main.jsx
├── .prettierrc
├── index.html
├── package.json
├── README.md
└── vite.config.js
export const setPizzas = pizzas => ({
type: 'PIZZAS/SET_ALL',
payload: pizzas
});
export const addPizza = pizza => ({
type: 'PIZZAS/ADD_ONE',
payload: pizza
});
export const upPizza = pizza => ({
type: 'PIZZAS/SET_ONE',
payload: pizza
});
export const delPizza = uid => ({
type: 'PIZZAS/DEL_ONE',
payload: uid
});
export const resetPizzas = (): { type: string } => ({
type: 'PIZZAS/RESET_ALL'
});
actions.js
const initialState = [
{
uid: 'c4lz0n3',
label: 'Calzone'
}
];
export default function pizzasReducer(state = initialState, action) {
switch (action.type) {
case 'PIZZAS/SET_ALL':
return action.payload;
case 'PIZZAS/ADD_ONE':
return [...state, action.payload];
case 'PIZZAS/SET_ONE':
return state.map(p => (p.uid === action.payload.uid ? action.payload : p));
case 'PIZZAS/DEL_ONE':
return state.filter(p => p.uid !== action.payload);
case 'PIZZAS/RESET_ALL':
return initialState;
default:
return state;
}
}
reducer.js
import { combineReducers, applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import pizzasReducer from './pizzas/reducer';
const rootReducer = combineReducers({
pizzas: pizzasReducer
});
export const configureStore = (defaultState = {}) => {
const composeEnhancers = applyMiddleware(thunk);
return createStore(rootReducer, defaultState, composeEnhancers);
};
store/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import App from './App';
import { configureStore } from './store';
import './index.css';
const store = configureStore();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
main.jsx
import { useDispatch, useSelector } from 'react-redux';
import { addPizza, delPizza } from './store/pizzas/actions';
export default function App() {
const dispatch = useDispatch();
const pizzas = useSelector(state => state.pizzas);
const isEmpty = useSelector(state => state.pizzas.length === 0);
const handleClick = () => dispatch(
addPizza({
uid: 'v3gg13',
label: 'Veggie'
})
);
return (
<div className="app">
<ul>
{pizzas.map(pizza => <li key={pizza.uid}>{pizza.label}</li>)}
</ul>
<div className="button-group">
<button onClick={handleClick}>Add Veggie</button>
<button onClick={() => dispatch(delPizza('v3gg13'))}>Del Veggie</button>
</div>
</div>
);
}
App.jsx
/
├── public/
├── src/
│ ├── contexts/
│ │ └── AppContext.jsx
│ ├── models/
│ ├── App.jsx
│ ├── index.css
│ ├── main.jsx
├── .prettierrc
├── index.html
├── package.json
├── README.md
└── vite.config.js
const initialState = [
{
uid: 'c4lz0n3',
label: 'Calzone'
}
];
const pizzasReducer = (state = initialState, action) => {
switch (action.type) {
case 'PIZZAS/SET_ALL':
return action.payload;
case 'PIZZAS/ADD_ONE':
return [...state, action.payload];
case 'PIZZAS/SET_ONE':
return state.map(p => (p.uid === action.payload.uid ? action.payload : p));
case 'PIZZAS/DEL_ONE':
return state.filter(p => p.uid !== action.payload);
case 'PIZZAS/RESET_ALL':
return initialState;
default:
return state;
}
};
AppContext.js
import { createContext, useContext, useReducer } from 'react';
// ...
const AppContext = createContext(initialState);
export const useAppContext = () => useContext(AppContext);
export default function AppProvider({ children }) {
const [state, dispatch] = useReducer(pizzasReducer, initialState);
const setPizzas = pizzas => dispatch({ type: 'PIZZAS/SET_ALL', payload: pizzas });
const addPizza = pizza => dispatch({ type: 'PIZZAS/ADD_ONE', payload: pizza });
const upPizza = pizza => dispatch({ type: 'PIZZAS/SET_ONE', payload: pizza });
const delPizza = uid => dispatch({ type: 'PIZZAS/DEL_ONE', payload: uid });
const resetPizzas = () => dispatch({ type: 'PIZZAS/RESET_ALL' });
const value = [state, { setPizzas, addPizza, upPizza, delPizza, resetPizzas }];
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}
AppContext.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import AppProvider from './contexts/AppProvider';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<AppProvider>
<App />
</AppProvider>
);
main.jsx
import { useState } from 'react';
import { useAppContext } from './contexts/AppContext';
export default function App() {
const [pizzas, { addPizza, delPizza }] = useAppContext();
const handleClick = () => addPizza({
uid: 'v3gg13',
label: 'Veggie'
});
return (
<div className="app">
<ul>
{pizzas.map(pizza => <li key={pizza.uid}>{pizza.label}</li>)}
</ul>
<div className="button-group">
<button onClick={handleClick}>Add Veggie</button>
<button onClick={() => delPizza('v3gg13')}>Del Veggie</button>
</div>
</div>
);
}
App.jsx
Redux
Context API
Concept
Flux de données unidirectionnel
Higher-Order Component
Apprentissage
Complex
Simple
Scalabilité
++
++
Maintenabilité
+
+
Renders
-
(Immutability)
+
Debug
+++
+
import { useRef } from 'react';
function useSignal(initialValue) {
const ref = useRef(initialValue);
const get = () => ref.current;
const set = () => {
ref.current = typeof val === 'function' ? val(ref.current) : val;
};
return [get, set];
}
export default function App() {
const [getCounter, setCounter] = useSignal(0);
const increment = () => setCounter(getCounter() + 1);
const decrement = () => setCounter(getCounter() - 1);
return (
<div className="app">
{/* THIS IS NOT WORKING */}
<span>Value: {getCounter()}</span>
<div className="button-group">
<button onClick={decrement}>-1</button>
<button onClick={increment}>+1</button>
</div>
</div>
);
}
/
├── public/
├── src/
│ ├── App.jsx
│ ├── index.css
│ ├── main.jsx
│ ├── store.js
├── .prettierrc
├── index.html
├── package.json
├── README.md
└── vite.config.js
npm install @preact/signals-react
import { signal, computed } from '@preact/signals-react';
export const pizzas = signal([
{
uid: 'c4lz0n3',
label: 'Calzone'
}
]);
export const isEmpty = computed(() => pizzas.value.length === 0);
export const setPizzas = pizzas => (pizzas.value = pizzas);
export const addPizza = pizza => {
pizzas.value = [...pizzas.value, pizza];
};
export const upPizza = pizza => {
pizzas.value = pizzas.value.map(p => (p.uid === pizza.uid ? pizza : p));
};
export const delPizza = uid => {
pizzas.value = pizzas.value.filter(p => p.uid !== uid);
};
export const resetPizzas = () => (pizzas.value = []);
store.js
import { useState } from 'react';
import { addPizza, delPizza, pizzas } from './store';
export default function App() {
const handleClick = () => addPizza({
uid: 'v3gg13',
label: 'Veggie'
});
return (
<div className="app">
<ul>
{pizzas.value.map(pizza => <li key={pizza.uid}>{pizza.label}</li>)}
</ul>
<div className="button-group">
<button onClick={handleClick}>Add Veggie</button>
<button onClick={() => delPizza('v3gg13')}>Del Veggie</button>
</div>
</div>
);
}
App.jsx
Component
State
With Signals
Component
.value
Nécessite de la rigueur lors de la mise à l'échelle
⚠️ Attention à React Router (une issue en version 1.2.X)
KISS - Keep It Simple Stupid
Faire du "neuf", avec du "vieux" (KnockoutJS) :
observable (state)
computed (side effect)
pureComputed (derivated state)
Easy to test 👍