useOptimistic,
который позволяет улучшить пользовательский опыт, прогнозируя изменения данных до их завершения.
useOptimistic
— это хук, который используется для создания оптимистичного UI. Оптимистичный UI означает, что мы сразу отображаем изменения в интерфейсе, предполагая, что операция на сервере пройдет успешно. Это помогает избежать задержек в UI и делает приложение более отзывчивым.import { useOptimistic, useState, useRef } from "react";
import { deliverMessage } from "./actions.js"; //Promise отправки
function Thread({ messages, sendMessage }) {
const formRef = useRef();
async function formAction(formData) {
addOptimisticMessage(formData.get("message"));
formRef.current.reset();
await sendMessage(formData);
}
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [
...state,
{
text: newMessage,
sending: true
}
]
);
return (
<>
{optimisticMessages.map((message, index) => (
<div key={index}>
{message.text}
{!!message.sending && <small> (Sending...)</small>}
</div>
))}
<form action={formAction} ref={formRef}>
<input type="text" name="message" placeholder="Hello!" />
<button type="submit">Send</button>
</form>
</>
);
}
export default function App() {
const [messages, setMessages] = useState([
{ text: "Hello there!", sending: false, key: 1 }
]);
async function sendMessage(formData) {
const sentMessage = await deliverMessage(formData.get("message"));
setMessages((messages) => [...messages, { text: sentMessage }]);
}
return <Thread messages={messages} sendMessage={sendMessage} />;
}
useOptimistic
позволяет сообщению сразу появиться в списке с меткой "Sending...", даже до того, как сообщение на самом деле будет отправлено на сервер. Этот «оптимистичный» подход создает впечатление скорости и отзывчивости. Затем форма пытается отправить сообщение в фоновом режиме. Как только сервер подтверждает, что сообщение было получено, метка "Sending..." удаляется.
useOptimistic
— это отличный инструмент для оптимизации работы с асинхронными запросами и обеспечения плавного пользовательского опыта. Используйте его, чтобы улучшить отзывчивость вашего приложения и показать пользователю, что он может продолжать работать, даже если процесс еще не завершен.import React from "react";
type PolymorphicProps<E extends React.ElementType> = {
as?: E; // Тип HTML-элемента или компонента
} & React.ComponentProps<E>; // Наследуем все стандартные пропсы элемента
const PolymorphicComponent = <E extends React.ElementType = "button">({
as,
children,
...props
}: PolymorphicProps<E>) => {
const Component = as || "button";
return <Component {...props}>{children}</Component>;
};
const App = () => {
return (
<>
<PolymorphicComponent onClick={() => alert("Button clicked!")}>
Кнопка
</PolymorphicComponent>
<PolymorphicComponent as="a" href="https://react.dev">
Ссылка
</PolymorphicComponent>
</>
);
};
export default App;
<E extends React.ElementType>
), чтобы автоматически применять подходящие типы пропсов для выбранного элемента.npm install @tanstack/react-table
import React, { useState } from "react";
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table'
const defaultData = [
{
firstName: 'tanner',
lastName: 'linsley',
age: 24,
visits: 100,
status: 'In Relationship',
progress: 50,
},
{
firstName: 'tandy',
lastName: 'miller',
age: 40,
visits: 40,
status: 'Single',
progress: 80,
},
{
firstName: 'joe',
lastName: 'dirte',
age: 45,
visits: 20,
status: 'Complicated',
progress: 10,
},
]
const columnHelper = createColumnHelper()
const columns = [
columnHelper.accessor('firstName', {
cell: info => info.getValue(),
}),
columnHelper.accessor(row => row.lastName, {
id: 'lastName',
cell: info => <i>{info.getValue()}</i>,
header: () => <span>Last Name</span>,
}),
columnHelper.accessor('age', {
header: () => 'Age',
cell: info => info.renderValue(),
}),
columnHelper.accessor('visits', {
header: () => <span>Visits</span>,
}),
columnHelper.accessor('status', {
header: 'Status',
}),
columnHelper.accessor('progress', {
header: 'Profile Progress',
}),
]
function App() {
const [data, _setData] = useState(() => [...defaultData])
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<div>
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
export default App;
service-worker.js
настроен. index.js
закомментируйте строку: serviceWorker.register();
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
registerRoute(
({ url }) => url.origin === 'https://api.example.com',
new StaleWhileRevalidate()
);
try {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
} catch (error) {
setData(localStorage.getItem('offlineData'));
}
navigator.onLine
для отслеживания статуса подключения. useEffect(() => {
const updateStatus = () => {
setIsOnline(navigator.onLine);
};
window.addEventListener('online', updateStatus);
window.addEventListener('offline', updateStatus);
return () => {
window.removeEventListener('online', updateStatus);
window.removeEventListener('offline', updateStatus);
};
}, []);
useFieldArray
и Controller
станет идеальным решением. Этот подход особенно полезен, когда вы работаете с управляемыми компонентами или сторонними библиотеками UIuseFieldArray
для управления массивом данных. Controller
. import React from 'react';
import { useForm, useFieldArray, Controller } from 'react-hook-form';
function DynamicForm() {
const { control, handleSubmit } = useForm({
defaultValues: {
users: [{ name: '', age: '' }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: 'users',
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id} style={{ marginBottom: '10px' }}>
<Controller
name={`users.${index}.name`}
control={control}
render={({ field }) => (
<input {...field} placeholder="Имя" />
)}
/>
<Controller
name={`users.${index}.age`}
control={control}
render={({ field }) => (
<input {...field} placeholder="Возраст" type="number" />
)}
/>
<button type="button" onClick={() => remove(index)}>
Удалить
</button>
</div>
))}
<button type="button" onClick={() => append({ name: '', age: '' })}>
Добавить поле
</button>
<button type="submit">Отправить</button>
</form>
);
}
export default DynamicForm;
fields
— текущие элементы массива. append
— добавляет новый объект в массив. remove
— удаляет элемент по индексу. value
, onChange
и другие свойства с вашим компонентом. append
добавляется новый объект с полями, соответствующими вашему массиву. import { LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts';
const data = [
{ name: 'Янв', uv: 400 },
{ name: 'Фев', uv: 300 },
{ name: 'Март', uv: 500 },
];
const MyChart = () => (
<LineChart width={400} height={300} data={data}>
<CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="uv" stroke="#ff7300" />
</LineChart>
);
export default MyChart;
import { Line } from 'react-chartjs-2';
const data = {
labels: ['Янв', 'Фев', 'Март'],
datasets: [
{
label: 'Продажи',
data: [400, 300, 500],
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
},
],
};
const MyChart = () => <Line data={data} />;
export default MyChart;
import React, { useEffect, useRef } from 'react';
import * as d3 from 'd3';
const BarChart = () => {
const chartRef = useRef();
useEffect(() => {
const data = [10, 20, 30, 40, 50];
const svg = d3
.select(chartRef.current)
.attr('width', 400)
.attr('height', 200);
svg
.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', (_, i) => i * 50)
.attr('y', (d) => 200 - d * 4)
.attr('width', 40)
.attr('height', (d) => d * 4)
.attr('fill', 'teal');
}, []);
return <svg ref={chartRef}></svg>;
};
export default BarChart;
onDragStart
— инициирует перетаскивание. onDragOver
— позволяет элементу быть зоной для сброса. onDrop
— срабатывает при отпускании элемента. import React, { useState } from 'react';
const DragAndDrop = () => {
const [droppedItem, setDroppedItem] = useState('');
const handleDragStart = (e, data) => {
e.dataTransfer.setData('text/plain', data);
};
const handleDrop = (e) => {
e.preventDefault();
const data = e.dataTransfer.getData('text/plain');
setDroppedItem(data);
};
const handleDragOver = (e) => {
e.preventDefault();
};
return (
<div>
<div
draggable
onDragStart={(e) => handleDragStart(e, 'Перетащено!')}
style={{ width: '150px', padding: '10px', backgroundColor: '#f0f0f0', cursor: 'grab', marginBottom: '20px' }}
>
Перетащи меня!
</div>
<div
onDrop={handleDrop}
onDragOver={handleDragOver}
style={{ width: '200px', height: '100px', backgroundColor: '#e0e0e0' }}
>
Брось сюда!
</div>
{droppedItem && <p>Результат: {droppedItem}</p>}
</div>
);
};
export default DragAndDrop;
onDragStart
и onDrop
. npm install react-dnd react-dnd-html5-backend
import React from 'react';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
const ItemType = 'BOX';
const DraggableItem = ({ id }) => {
const [, dragRef] = useDrag(() => ({
type: ItemType,
item: { id },
}));
return (
<div ref={dragRef} style={{ padding: '10px', backgroundColor: '#f0f0f0', marginBottom: '10px', cursor: 'grab' }}>
Элемент {id}
</div>
);
};
const DropZone = ({ onDrop }) => {
const [, dropRef] = useDrop(() => ({
accept: ItemType,
drop: (item) => onDrop(item.id),
}));
return (
<div
ref={dropRef}
style={{ width: '200px', height: '100px', backgroundColor: '#e0e0e0' }}
>
Брось сюда!
</div>
);
};
const DragAndDropExample = () => {
const handleDrop = (id) => {
alert(`Вы перетащили элемент ${id}`);
};
return (
<DndProvider backend={HTML5Backend}>
<DraggableItem id={1} />
<DraggableItem id={2} />
<DropZone onDrop={handleDrop} />
</DndProvider>
);
};
export default DragAndDropExample;
react-dnd
useEffect,
— это важная часть разработки, особенно в компонентах, которые взаимодействуют с сервером. Давайте разберём основные подходы и инструменты, которые помогут вам писать надёжные тесты. import { setupServer } from 'msw/node';
import { rest } from 'msw';
const server = setupServer(
rest.get('/api/data', (req, res, ctx) => {
return res(ctx.json({ message: 'Success' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
import { render, screen, waitFor } from '@testing-library/react';
import App from './App';
test('показывает данные после успешного запроса', async () => {
render(<App />);
expect(screen.getByText(/загрузка/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/success/i)).toBeInTheDocument();
});
});
jest.fn().
const mockFetchData = jest.fn(() => Promise.resolve({ message: 'Success' }));
test('вызывает fetchData при рендере', async () => {
render(<App fetchData={mockFetchData} />);
expect(mockFetchData).toHaveBeenCalledTimes(1);
});
useEffect
часто используется для выполнения API-запросов, и важно убедиться, что компонент правильно реагирует на изменения. import { render, screen } from '@testing-library/react';
import App from './App';
test('выполняет побочный эффект при монтировании', () => {
render(<App />);
expect(screen.getByText(/загрузка/i)).toBeInTheDocument();
});
test('отменяет запрос при размонтировании', () => {
const mockFetchData = jest.fn(() => Promise.resolve());
const { unmount } = render(<App fetchData={mockFetchData} />);
unmount();
expect(mockFetchData).toHaveBeenCalledTimes(1);
});
AbortController
позволяет отменять запросы. import React, { useEffect } from 'react';
function App() {
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch('https://api.example.com/posts', { signal })
.then((response) => response.json())
.then((data) => console.log(data))
.catch((err) => {
if (err.name === 'AbortError') {
console.log('Запрос отменён');
} else {
console.error('Ошибка:', err);
}
});
return () => {
controller.abort(); // Отменяем запрос при размонтировании
};
}, []);
return <h1>Отмена запросов с помощью AbortController</h1>;
}
export default App;
AbortController.
signal
в запрос fetch.
abort(),
чтобы отменить запрос. import React, { useEffect } from 'react';
import axios from 'axios';
function App() {
useEffect(() => {
const controller = new AbortController();
axios
.get('https://api.example.com/posts', {
signal: controller.signal,
})
.then((response) => console.log(response.data))
.catch((err) => {
if (axios.isCancel(err)) {
console.log('Запрос отменён');
} else {
console.error('Ошибка:', err);
}
});
return () => {
controller.abort();
};
}, []);
return <h1>Отмена запросов с Axios</h1>;
}
export default App;
AbortController,
можно обойтись флагами, хотя это менее элегантно. import React, { useEffect, useState } from 'react';
function App() {
const [data, setData] = useState(null);
useEffect(() => {
let isMounted = true;
fetch('https://api.example.com/posts')
.then((response) => response.json())
.then((data) => {
if (isMounted) {
setData(data);
}
});
return () => {
isMounted = false; // Флаг при размонтировании
};
}, []);
return <div>{data ? <h1>Данные загружены</h1> : <h1>Загрузка...</h1>}</div>;
}
export default App;
npx create-react-app my-app --template typescript
import styles from './Button.module.css';
function Button() {
return <button className={styles.primary}>Click me</button>;
}
import logo from './logo.svg';
function App() {
return <img src={logo} alt="Logo" />;
}
npm run build
создаёт готовый к деплою проект с минимизацией файлов, оптимизацией CSS, и удалением ненужного кода. .env
и укажите там переменные, например: REACT_APP_API_URL=https://api.example.com
process.env.REACT_APP_API_URL
. npm test
npm run eject
. Это сделает конфигурацию доступной для редактирования, но будьте осторожны: отменить eject
невозможно! import { counterReducer } from './counterSlice';
describe('counterReducer', () => {
it('должен увеличивать значение', () => {
const initialState = { count: 0 };
const action = { type: 'counter/increment' };
const result = counterReducer(initialState, action);
expect(result).toEqual({ count: 1 });
});
it('должен уменьшать значение', () => {
const initialState = { count: 2 };
const action = { type: 'counter/decrement' };
const result = counterReducer(initialState, action);
expect(result).toEqual({ count: 1 });
});
});
import { increment, decrement } from './counterSlice';
describe('actions', () => {
it('increment должен возвращать правильный action', () => {
expect(increment()).toEqual({ type: 'counter/increment' });
});
it('decrement должен возвращать правильный action', () => {
expect(decrement()).toEqual({ type: 'counter/decrement' });
});
});
redux-thunk
) import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { fetchData } from './dataActions';
import { fetchDataSuccess } from './dataSlice';
const mockStore = configureMockStore([thunk]);
describe('async actions', () => {
it('должен диспатчить fetchDataSuccess после успешного API-запроса', async () => {
const store = mockStore({});
const mockResponse = { data: 'test' };
// Мокаем fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockResponse),
})
);
await store.dispatch(fetchData());
const actions = store.getActions();
expect(actions[0]).toEqual(fetchDataSuccess(mockResponse));
});
});
Provider
с тестовым хранилищем. Provider
import React from 'react';
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import Counter from './Counter';
const mockStore = configureMockStore([]);
describe('Counter component', () => {
it('должен отображать начальное значение', () => {
const store = mockStore({ counter: { count: 0 } });
const { getByText } = render(
<Provider store={store}>
<Counter />
</Provider>
);
expect(getByText('Count: 0')).toBeInTheDocument();
});
});
jest.fn()
или библиотеки, такие как msw. ReactDOM.createPortal
. ReactDOM.createPortal(child, container)
<div id="modal-root">
. <div id="modal-root"></div>
import React from 'react';
import ReactDOM from 'react-dom';
const Modal = ({ isOpen, onClose, children }) => {
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={onClose}>×</button>
{children}
</div>
</div>,
document.getElementById('modal-root')
);
};
export default Modal;
#modal-root
. import React, { useState } from 'react';
import Modal from './Modal';
function App() {
const [isModalOpen, setModalOpen] = useState(false);
return (
<div>
<button onClick={() => setModalOpen(true)}>Открыть модальное окно</button>
<Modal isOpen={isModalOpen} onClose={() => setModalOpen(false)}>
<h1>большой привет от ReactJs Daily!</h1>
<p>Подпишись на канал)</p>
</Modal>
</div>
);
}
export default App;
npx storybook@latest init
npm run storybook
import React from 'react';
const Button = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);
export default Button;
import React from 'react';
import Button from './Button';
export default {
title: 'Example/Button', // Название компонента
component: Button, // Компонент
};
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
label: 'Primary Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
label: 'Secondary Button',
};
npm install @storybook/addon-controls
npx create-next-app@latest my-nextjs-app
cd my-nextjs-app
npm run dev
pages/
— папка для маршрутов и страниц. public/
— статические файлы (изображения, шрифты и т.д.). styles/
— глобальные и модульные стили. .js
или .tsx
файл в этой папке автоматически становится маршрутом. about.js
доступен по /about
. // pages/post/[id].js
import { useRouter } from 'next/router';
export default function Post() {
const { query } = useRouter();
return <div>Post ID: {query.id}</div>;
}
logo.png
в этой папке будет доступен по /logo.png
. globals.css
) и модульные стили (Component.module.css
). pages/
соответствует URL-адресам. // pages/about.js
export default function About() {
return <h1>About Page</h1>;
}
/about
. // pages/product/[id].js
import { useRouter } from 'next/router';
export default function Product() {
const { query } = useRouter();
return <h1>Product ID: {query.id}</h1>;
}
/product/123
. <Image>
автоматически обрабатывает загрузку и размеры. import { render, screen } from "@testing-library/react";
import MyComponent from "./MyComponent";
test("renders the title", () => {
render(<MyComponent />);
const title = screen.getByText(/Hello, World!/i);
expect(title).toBeInTheDocument();
});
getBy*
, queryBy*
и findBy*
в зависимости от контекста: getBy*
— для синхронного поиска. findBy*
— для асинхронных тестов. queryBy*
— когда элемент может отсутствовать. import { render, screen } from "@testing-library/react";
import MyButton from "./MyButton";
test("renders button with the correct label", () => {
render(<MyButton label="Click me" />);
const button = screen.getByText(/Click me/i);
expect(button).toBeInTheDocument();
});
import { render, screen, fireEvent } from "@testing-library/react";
import Counter from "./Counter";
test("increments counter on button click", () => {
render(<Counter />);
const button = screen.getByRole("button", { name: /increment/i });
fireEvent.click(button);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
fireEvent
для сложных взаимодействий используйте user-event — это ближе к реальным сценариям. import { render, screen } from "@testing-library/react";
import ConditionalComponent from "./ConditionalComponent";
test("renders message when condition is true", () => {
render(<ConditionalComponent show={true} />);
expect(screen.getByText(/Condition is true/i)).toBeInTheDocument();
});
test("does not render message when condition is false", () => {
render(<ConditionalComponent show={false} />);
expect(screen.queryByText(/Condition is true/i)).toBeNull();
});
import renderer from "react-test-renderer";
import MyComponent from "./MyComponent";
test("matches the snapshot", () => {
const tree = renderer.create(<MyComponent />).toJSON();
expect(tree).toMatchSnapshot();
});
useLayoutEffect
— это хук, который запускается синхронно после всех изменений в DOM. В отличие от useEffect
, он срабатывает до того, как браузер отрисует кадр, что делает его полезным для измерения и изменения DOM. import { useLayoutEffect, useRef } from "react";
function ScrollToBottom() {
const containerRef = useRef(null);
useLayoutEffect(() => {
const container = containerRef.current;
if (container) {
container.scrollTop = container.scrollHeight; // Прокрутка до конца
}
}, []); // Пустой массив зависимостей — эффект выполняется один раз
return (
<div
ref={containerRef}
style={{ height: "200px", overflow: "auto", border: "1px solid black" }}
>
<p>Содержимое...</p>
<p>Много текста...</p>
</div>
);
}
useLayoutEffect
гарантирует, что прокрутка произойдёт *до* отрисовки обновлённого интерфейса.useEffect
выполняется асинхронно, после отрисовки. useLayoutEffect
блокирует отрисовку, поэтому может замедлить загрузку страницы при избыточном использовании. useLayoutEffect
только тогда, когда useEffect
не подходит, например, если вы видите "мерцания" в интерфейсе из-за запоздалых измерений или изменений DOM. useEffect
. useLayoutEffect
.interface User {
id: number;
name: string;
email: string;
}
const user: User = {
id: 1,
name: 'John Doe',
email: 'john.doe@example.com',
};
User
: id
, name
и email
. interface User {
id: number;
name: string;
email?: string; // Опциональное свойство
}
const userWithoutEmail: User = {
id: 2,
name: 'Jane Smith',
};
extends
: interface User {
id: number;
name: string;
}
interface Admin extends User {
role: string;
}
const admin: Admin = {
id: 1,
name: 'Admin User',
role: 'Super Admin',
};
interface ButtonProps {
label: string;
onClick: () => void;
}
const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);