Considérons la création d'une version Todolist d'une application React Hooks à l'aide de TypeScript .
Assemblée
La structure du projet est la suivante:
âââ src
| âââ composants
| âââ index.html
| âââ index.tsx
âââ package.json
âââ tsconfig.json
âââ webpack.config.json
Fichier Package.json:
TypeScript, typescript, ts-loader, tsx- js-, React â @types/react @types/react-dom. html-webpack-plugin, dev- index.html â , production- .
{
"name": "todo-react-typescript",
"version": "1.0.0",
"description": "",
"main": "index.tsx",
"scripts": {
"start": "webpack-dev-server --port 3000 --mode development --open --hot",
"build": "webpack --mode production"
},
"author": "",
"license": "ISC",
"devDependencies": {
"ts-loader": "^5.2.1",
"html-webpack-plugin": "^3.2.0",
"typescript": "^3.8.2",
"webpack": "^4.41.6",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
},
"dependencies": {
"@types/react": "^16.9.23",
"@types/react-dom": "^16.9.5",
"react": "^16.12.0",
"react-dom": "^16.12.0"
}
}
TypeScript, typescript, ts-loader, tsx- js-, React â @types/react @types/react-dom. html-webpack-plugin, dev- index.html â , production- .
Fichier Tsconfig.json:
«jsx» . 3 : «preserve», «react» «react-native».
{
"compilerOptions": {
"sourceMap": true,
"noImplicitAny": false,
"module": "commonjs",
"target": "es6",
"lib": [
"es2015",
"es2017",
"dom"
],
"removeComments": true,
"allowSyntheticDefaultImports": false,
"jsx": "react",
"allowJs": true,
"baseUrl": "./",
"paths": {
"components/*": [
"src/components/*"
]
}
}
}
«jsx» . 3 : «preserve», «react» «react-native».
Fichier Webpack.config.json:
â ./src/index.tsx. resolve.extensions ts/tsx/js . ts-loader html-webpack-plugin. .
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.tsx',
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
output: {
path: path.join(__dirname, '/dist'),
filename: 'bundle.min.js'
},
module: {
rules: [
{
test: /\.ts(x?)$/,
exclude: /node_modules/,
use: [
{
loader: "ts-loader"
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
};
â ./src/index.tsx. resolve.extensions ts/tsx/js . ts-loader html-webpack-plugin. .
DĂ©veloppement de
Dans le fichier index.html, nous enregistrons le conteneur dans lequel l'application sera rendue:
<div id="root"></div>
Dans le répertoire des composants, créez notre premier composant vide, App.tsx.
Fichier Index.tsx:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from "./components/App";
ReactDOM.render (
<App/>,
document.getElementById("root")
);
L'application Todolist aura les fonctionnalités suivantes:
- Ajouter une tĂąche
- supprimer la tĂąche
- changer l'état de la tùche (terminé / non terminé)
Cela ressemblera à ceci: un champ de texte pour l'entrée + le bouton Ajouter une tùche, et ci-dessous une liste de tùches ajoutées. Vous pouvez supprimer des tùches et modifier leur statut.
à ces fins, vous pouvez diviser l'application en seulement deux composants: créer une nouvelle tùche et une liste de toutes les tùches. Par conséquent, App.tsx au stade initial ressemblera à ceci:
import * as React from 'react';
import NewTask from "./NewTask";
import TasksList from "./TasksList";
const App = () => {
return (
<>
<NewTask />
<TasksList />
</>
)
}
export default App;
Dans le répertoire courant, créez et exportez des composants NewTask et TasksList vides. Puisque nous devons assurer la relation entre eux, nous devons déterminer comment cela se produira. Il existe deux approches de communication entre les composants dans React:
- Stocker l'état actuel de l'application et toutes ses méthodes dans le composant parent (dans notre cas, dans App.tsx) et le transmettre aux composants enfants via des accessoires (la maniÚre classique);
- SĂ©parer les mĂ©thodes de gestion des Ă©tats et des Ă©tats. Dans ce cas, l'application doit ĂȘtre encapsulĂ©e avec un composant spĂ©cial - un fournisseur, et les mĂ©thodes et propriĂ©tĂ©s nĂ©cessaires pour les composants enfants doivent lui ĂȘtre transmises (Ă l'aide du hook useContext).
Nous utiliserons la deuxiÚme méthode et dans cet exemple, nous abandonnerons complÚtement les accessoires.
TypeScript lors du passage des accessoires
* , TypeScript :
React.FC, , ( ) :
const NewTask: React.FC<MyProps> = ({taskName}) => {...
React.FC, , ( ) :
interface MyProps {
taskName: String;
}
useContext
Donc, pour transférer l'état, nous utiliserons le hook useContext. Il vous permet d'obtenir et de modifier des données dans l'un des composants encapsulés par le fournisseur.
Exemple UseContext
â name surname, String.
createContext . , TypeScript « » , Partial â .
â person, . , . useContext.
import * as React from 'react';
import {useContext} from "react";
interface Person {
name: String,
surname: String
}
export const PersonContext = React.createContext<Partial<Person>>({});
const PersonWrapper = () => {
const person: Person = {
name: 'Spider',
surname: 'Man'
}
return (
<>
<PersonContext.Provider value={ person }>
<PersonComponent />
</PersonContext.Provider>
</>
)
}
const PersonComponent = () => {
const person = useContext(PersonContext);
return (
<div>
Hello, {person.name} {person.surname}!
</div>
)
}
export default PersonWrapper;
â name surname, String.
createContext . , TypeScript « » , Partial â .
â person, . , . useContext.
useReducer
Vous aurez Ă©galement besoin de useReducer pour un travail plus pratique avec le magasin d'Ă©tat.
En savoir plus sur useReducer
useReducer , : , type, â payload. :
useReducer - personReducer, changePerson.
person initialState, changePerson .
CHANGE, , :
import * as React from 'react';
import {useReducer} from "react";
interface PersonState {
name: String,
surname: String
}
interface PersonAction {
type: 'CHANGE',
payload: PersonState
}
const personReducer = (state: PersonState, action: PersonAction): PersonState => {
switch (action.type) {
case 'CHANGE':
return action.payload;
default: throw new Error('Unexpected action');
}
}
const PersonComponent = () => {
const initialState = {
name: 'Unknown',
surname: 'Guest'
}
const [person, changePerson] = useReducer<React.Reducer<PersonState, PersonAction>>(personReducer, initialState);
return (
<div onClick={() => changePerson({type: 'CHANGE', payload: {name: 'Jackie', surname: 'Chan'}})}>
Hello, {person.name} {person.surname}!
</div>
)
}
export default PersonComponent;
useReducer - personReducer, changePerson.
person initialState, changePerson .
CHANGE, , :
case 'CHANGE':
return action.payload;
case 'CLEAR':
return {
name: 'Undefined',
surname: 'Undefined'
};
useContext + useReducer
Un remplacement intĂ©ressant pour la bibliothĂšque Redux peut ĂȘtre l'utilisation de contexte en conjonction avec useReducer. Dans ce cas, le rĂ©sultat du hook useReducer - l'Ă©tat retournĂ© et la fonction de mise Ă jour - sera passĂ© au contexte. Ajoutons ces hooks Ă l'application:
import * as React from 'react';
import {useReducer} from "react";
import {Action, State, ContextState} from "../types/stateType";
import NewTask from "./NewTask";
import TasksList from "./TasksList";
//
export const initialState: State = {
newTask: '',
tasks: []
}
// <Partial>
export const ContextApp = React.createContext<Partial<ContextState>>({});
// , Action type payload, - State
export const todoReducer = (state: State, action: Action):State => {
switch (action.type) {
case ActionType.ADD: {
return {...state, tasks: [...state.tasks, {
name: action.payload,
isDone: false
}]}
}
case ActionType.CHANGE: {
return {...state, newTask: action.payload}
}
case ActionType.REMOVE: {
return {...state, tasks: [...state.tasks.filter(task => task !== action.payload)]}
}
case ActionType.TOGGLE: {
return {...state, tasks: [...state.tasks.map((task) => (task !== action.payload ? task : {...task, isDone: !task.isDone}))]}
}
default: throw new Error('Unexpected action');
}
};
const App: React.FC = () => {
// todoReducer, useReduser. initialState, (changeState) .
const [state, changeState] = useReducer<React.Reducer<State, Action>>(todoReducer, initialState);
const ContextState: ContextState = {
state,
changeState
};
// useReducer -
return (
<>
<ContextApp.Provider value={ContextState}>
<NewTask />
<TasksList />
</ContextApp.Provider>
</>
)
}
export default App;
En consĂ©quence, nous avons rĂ©ussi Ă crĂ©er un Ă©tat indĂ©pendant du composant racine, qui peut ĂȘtre reçu et modifiĂ© dans les composants du fournisseur.
Manuscrit. Ajout de types Ă l'application
Dans le fichier stateType, nous Ă©crivons les types TypeScript pour l'application:
import {Dispatch} from "react";
//
export type Task = {
name: string;
isDone: boolean
}
export type Tasks = Task[];
// ,
export type State = {
newTask: string;
tasks: Tasks
}
//
export enum ActionType {
ADD = 'ADD',
CHANGE = 'CHANGE',
REMOVE = 'REMOVE',
TOGGLE = 'TOGGLE'
}
// ADD CHANGE
type ActionStringPayload = {
type: ActionType.ADD | ActionType.CHANGE,
payload: string
}
// TOGGLE REMOVE Task
type ActionObjectPayload = {
type: ActionType.TOGGLE | ActionType.REMOVE,
payload: Task
}
//
export type Action = ActionStringPayload | ActionObjectPayload;
// -, Action. Dispatch react
export type ContextState = {
state: State;
changeState: Dispatch<Action>
}
Utiliser le contexte
L'Ă©tat est maintenant prĂȘt et peut ĂȘtre utilisĂ© dans les composants. Commençons par NewTask.tsx:
import * as React from 'react';
import {useContext} from "react";
import {ContextApp} from "./App";
import {TaskName} from "../types/taskType";
import {ActionType} from "../types/stateType";
const NewTask: React.FC = () => {
// state dispatch-
const {state, changeState} = useContext(ContextApp);
// todoReducer - . state . React-
const addTask = (event: React.FormEvent<HTMLFormElement>, task: TaskName) => {
event.preventDefault();
changeState({type: ActionType.ADD, payload: task})
changeState({type: ActionType.CHANGE, payload: ''})
}
// -
const changeTask = (event: React.ChangeEvent<HTMLInputElement>) => {
changeState({type: ActionType.CHANGE, payload: event.target.value})
}
return (
<>
<form onSubmit={(event)=>addTask(event, state.newTask)}>
<input type='text' onChange={(event)=>changeTask(event)} value={state.newTask}/>
<button type="submit">Add a task</button>
</form>
</>
)
};
export default NewTask;
TasksList.tsx:
import * as React from 'react';
import {Task} from "../types/taskType";
import {ActionType} from "../types/stateType";
import {useContext} from "react";
import {ContextApp} from "./App";
const TasksList: React.FC = () => {
// ( changeState)
const {state, changeState} = useContext(ContextApp);
const removeTask = (taskForRemoving: Task) => {
changeState({type: ActionType.REMOVE, payload: taskForRemoving})
}
const toggleReadiness = (taskForChange: Task) => {
changeState({type: ActionType.TOGGLE, payload: taskForChange})
}
return (
<>
<ul>
{state.tasks.map((task,i)=>(
<li key={i} className={task.isDone ? 'ready' : null}>
<label>
<input type="checkbox" onChange={()=>toggleReadiness(task)} checked={task.isDone}/>
</label>
<div className="task-name">
{task.name}
</div>
<button className='remove-button' onClick={()=>removeTask(task)}>
X
</button>
</li>
))}
</ul>
</>
)
};
export default TasksList;
L'application est prĂȘte! Il reste Ă le tester.
Essai
Pour les tests, Jest + Enzyme sera utilisé ainsi que @ testing-library / react .
Vous devez installer les dépendances de développement:
"@testing-library/react": "^10.4.3",
"@testing-library/react-hooks": "^3.3.0",
"@types/enzyme": "^3.10.5",
"@types/jest": "^24.9.1",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.3.4",
"jest": "^26.1.0",
"ts-jest": "^26.1.1",
Ajoutez des paramĂštres pour jest Ă package.json:
"jest": {
"preset": "ts-jest",
"setupFiles": [
"./src/__tests__/setup.ts"
],
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"testRegex": "/__tests__/.*\\.test.(ts|tsx)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
},
et dans le bloc "scripts" ajoutez un script pour exécuter les tests:
"test": "jest"
Créez un nouveau répertoire __tests__ dans le répertoire src et dans celui-ci un fichier setup.ts avec le contenu suivant:
import {configure} from 'enzyme';
import * as ReactSixteenAdapter from 'enzyme-adapter-react-16';
const adapter = ReactSixteenAdapter as any;
configure({ adapter: new adapter() });
Créons un fichier todoReducer.test.ts dans lequel nous testerons le réducteur:
import {todoReducer} from "../reducers/todoReducer";
import {ActionType, Action, State} from "../types/stateType";
import {Task} from "../types/taskType";
describe('todoReducer',()=>{
it('returns new state for "ADD" type', () => {
//
const initialState: State = {newTask: '', tasks: []};
// 'ADD' 'new task'
const updateAction: Action = {type: ActionType.ADD, payload: 'new task'};
//
const updatedState = todoReducer(initialState, updateAction);
//
expect(updatedState).toEqual({newTask: '', tasks: [{name: 'new task', isDone: false}]});
});
it('returns new state for "REMOVE" type', () => {
const task: Task = {name: 'new task', isDone: false}
const initialState: State = {newTask: '', tasks: [task]};
const updateAction: Action = {type: ActionType.REMOVE, payload: task};
const updatedState = todoReducer(initialState, updateAction);
expect(updatedState).toEqual({newTask: '', tasks: []});
});
it('returns new state for "TOGGLE" type', () => {
const task: Task = {name: 'new task', isDone: false}
const initialState: State = {newTask: '', tasks: [task]};
const updateAction: Action = {type: ActionType.TOGGLE, payload: task};
const updatedState = todoReducer(initialState, updateAction);
expect(updatedState).toEqual({newTask: '', tasks: [{name: 'new task', isDone: true}]});
});
it('returns new state for "CHANGE" type', () => {
const initialState: State = {newTask: '', tasks: []};
const updateAction: Action = {type: ActionType.CHANGE, payload: 'new task'};
const updatedState = todoReducer(initialState, updateAction);
expect(updatedState).toEqual({newTask: 'new task', tasks: []});
});
})
Pour tester le réducteur, il suffit de lui transmettre l'état et l'action actuels, puis de récupérer le résultat de son exécution.
Le test du composant App.tsx, contrairement au réducteur, nécessite l'utilisation de méthodes supplémentaires provenant de différentes bibliothÚques. Fichier test App.test.tsx:
import * as React from 'react';
import {shallow} from 'enzyme';
import {fireEvent, render, cleanup} from "@testing-library/react";
import App from "../components/App";
describe('<App />', () => {
// jest- afterEach cleanup
afterEach(cleanup);
it('hasn`t got changes', () => {
// shallow enzyme -, .
const component = shallow(<App />);
// . . snapshots -u: jest -u
expect(component).toMatchSnapshot();
});
// ( DOM-), async
it('should render right input value', async () => {
// render() @testing-library/react" shallow() , DOM- . container â div, .
const { container } = render(<App/>);
expect(container.querySelector('input').getAttribute('value')).toEqual('');
// 'test'
fireEvent.change(container.querySelector('input'), {
target: {
value: 'test'
},
})
// 'test'
expect(container.querySelector('input').getAttribute('value')).toEqual('test');
// .
fireEvent.click(container.querySelector('button'))
// value
expect(container.querySelector('input').getAttribute('value')).toEqual('');
});
})
Dans le composant TasksList, vérifiez si l'état passé s'affiche correctement. Fichier TasksList.test.tsx:
import * as React from 'react';
import {ContextApp, initialState} from "../components/App";
import {shallow} from "enzyme";
import {cleanup, render} from "@testing-library/react";
import TasksList from "../components/TasksList";
import {State} from "../types/stateType";
describe('<TasksList />',() => {
afterEach(cleanup);
//
const testState: State = {
newTask: '',
tasks: [{name: 'test', isDone: false}, {name: 'test2', isDone: false}]
}
// ContextApp
const Wrapper = () => {
return (
<ContextApp.Provider value={{state: testState}}>
<TasksList/>
</ContextApp.Provider>
)
}
it('should render right tasks length', async () => {
const {container} = render(<Wrapper/>);
//
expect(container.querySelectorAll('li')).toHaveLength(testState.tasks.length);
});
})
Une vĂ©rification similaire du champ newTask peut ĂȘtre effectuĂ©e pour le composant NewTask en vĂ©rifiant la valeur de l'Ă©lĂ©ment d'entrĂ©e.
Le projet peut ĂȘtre tĂ©lĂ©chargĂ© Ă partir du rĂ©fĂ©rentiel GitHub .
C'est tout, merci pour votre attention.
Ressources
RĂ©agissez JS. Hooks fonctionnant
avec les hooks React et TypeScript