03:15 ➝ ВСория

Π Π΅Π°ΠΊΡ‚ - это Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° для создания ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΡ… интСрфСйсов. Π’ΠΎ Π΅ΡΡ‚ΡŒ это ΠΎΠ·Π½Π°Ρ‡Π°Π΅Ρ‚, Ρ‡Ρ‚ΠΎ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΏΠΈΡΠ°Ρ‚ΡŒ интСрфСйсы Π½Π° Π½Ρ‘ΠΌ Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ для Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½ΠΎ ΠΈ для ΠΌΠΎΠ±ΠΈΠ»ΠΎΠΊ, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ ΠΎΠ½ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ свой Π²ΠΈΡ€Ρ‚ΡƒΠ°Π»ΡŒΠ½Ρ‹ΠΉ DOM.

ΠœΡ‹ ΠΈΠΌΠ΅Π΅ΠΌ Π΄Π²Π΅ основныС ΠΊΠΎΠ½Ρ†Π΅ΠΏΡ†ΠΈΠΈ сайтов:

  • MPA (Multi Page Application) - сайт состоит ΠΈΠ· Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… страниц, ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄Π΅ Π½Π° ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ ΠΏΠΎΠ΄Π³Ρ€ΡƒΠΆΠ°Π΅ΠΌ ΠΈΡ… Π΄Π°Π½Π½Ρ‹Π΅ Ρ†Π΅Π»ΠΈΠΊΠΎΠΌ
  • SPA (Single Page Application) - вСсь сайт располагаСтся Π½Π° ΠΎΠ΄Π½ΠΎΠΉ страницС ΠΈ ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄Π΅ Π½Π° Π΄Ρ€ΡƒΠ³ΡƒΡŽ страницу Π² Π½Ρ‘ΠΌ ΠΌΠ΅Π½ΡΡŽΡ‚ΡΡ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅

Π Π΅Π°ΠΊΡ‚ основан Π½Π° ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π½ΠΎΠΌ ΠΏΠΎΠ΄Ρ…ΠΎΠ΄Π΅, ΠΊΠΎΠ³Π΄Π° страница строится ΠΈΠ· ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹Ρ… ΠΊΠΈΡ€ΠΏΠΈΡ‡ΠΈΠΊΠΎΠ², ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ. Π’Π°ΠΊ ΠΆΠ΅ ΠΎΠ½ позволяСт ΡΠΎΡΡ€Π΅Π΄ΠΎΡ‚ΠΎΡ‡ΠΈΡ‚ΡŒΡΡ Π½Π° написании Π»ΠΎΠ³ΠΈΠΊΠΈ прилоТСния Π±Π΅Π· Ρ€Π°Π±ΠΎΡ‚Ρ‹ со ΡΠ»ΡƒΡˆΠ°Ρ‚Π΅Π»ΡΠΌΠΈ событий, нСпосрСдствСнной Ρ€Π°Π±ΠΎΡ‚Ρ‹ с DOM (querySelector ΠΈ ΠΏΠΎΠ΄ΠΎΠ±Π½Ρ‹Π΅ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ) - Ρ€Π΅Π°ΠΊΡ‚ Π±Π΅Ρ€Ρ‘Ρ‚ эту Ρ€Π°Π±ΠΎΡ‚Ρƒ Π½Π° сСбя.

Π’ΠΎ врСмя своСй Ρ€Π°Π±ΠΎΡ‚Ρ‹ Ρ€Π΅Π°ΠΊΡ‚ строит Π΄Π²Π° своих Π΄Π΅Ρ€Π΅Π²Π° ΠΈ пСрСносят измСнСния Π½Π° ΠΊΠΎΠ½Π΅Ρ‡Π½ΠΎΠ΅ Ρ‚Ρ€Π΅Ρ‚ΡŒΠ΅:

  • ΠŸΠ΅Ρ€Π²ΠΎΠ΅ - Π΄Π΅Ρ€Π΅Π²ΠΎ элСмСнтов Ρ€Π΅Π°ΠΊΡ‚Π° - ΠΊΠΎΠ³Π΄Π° Π² Π½Ρ‘ΠΌ происходят измСнСния, ΠΎΠ½ΠΈ ΠΏΠΎΠΏΠ°Π΄Π°ΡŽΡ‚ Π½Π° Π²Ρ‚ΠΎΡ€ΠΎΠ΅ Π΄Π΅Ρ€Π΅Π²ΠΎ, ΠΌΠ΅ΠΆΠ΄Ρƒ ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΌΠΈ происходит сравнСниС
  • Π’Ρ‚ΠΎΡ€ΠΎΠ΅ - Π²ΠΈΡ€Ρ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠ΅ Π΄Π΅Ρ€Π΅Π²ΠΎ для сравнСния
  • Π’Ρ€Π΅Ρ‚ΡŒΠ΅ - это ΠΊΠΎΠ½Π΅Ρ‡Π½Ρ‹ΠΉ DOM Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ ΠΈ вносятся измСнСния послС сравнСния (Ρ„Π°Π·Π° Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π°, Π·Π° ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ ΠΎΡ‚Π²Π΅Ρ‡Π°Π΅Ρ‚ React DOM ΠΈΠ»ΠΈ React Native)

ΠœΠ΅Ρ…Π°Π½ΠΈΠ·ΠΌ согласования (Reconciliation) осущСствляСт сравнСниС элСмСнтов Π΄Π΅Ρ€Π΅Π²Π° Ρ€Π΅Π°ΠΊΡ‚Π°. Π’Π°ΠΊ ΠΆΠ΅ Ρ€Π΅Π°ΠΊΡ‚ Π΄Π΅Π»ΠΈΡ‚ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ ΠΏΠΎ приоритСтности ΠΈ Π±ΠΎΠ»Π΅Π΅ ΠΏΡ€ΠΈΠΎΡ€ΠΈΡ‚Π΅Π½Ρ‹Π΅ Π·Π°Π΄Π°Ρ‡ΠΈ ΠΎΠ½ выполняСт быстрСС.

11:40 ➝ Начало Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ. Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°

Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Ρ€Π΅Π°ΠΊΡ‚-прилоТСния

npx create-react-app .

Запуск компиляции прилоТСния

npm start

ΠΠ°Ρ‡Π°Π»ΡŒΠ½ΠΎΠΉ страницСй, которая запускаСт вСсь Ρ€Π΅Π½Π΄Π΅Ρ€ прилоТСния являСтся index.js, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ рСндСрится Π² root Π΄ΠΈΠ²Π΅ index.html Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°

public > index.html

<body>
  <noscript>
	  You need to enable JavaScript to run this app.
  </noscript>
  <div id="root"></div>
</body>

src > index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
 
ReactDOM.render(
    <App/>,
  document.getElementById('root')
);

16:10 ➝ Π§Ρ‚ΠΎ Ρ‚Π°ΠΊΠΎΠ΅ JSX?

JSX - это прСпроцСссор, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ babel ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄ΠΈΡ‚ Π² ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹ΠΉ JS

18:11 ➝ ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ App. Π Π°Π±ΠΎΡ‚Π° с состояниСм. UseState

Π—Π°Π΄Π°Ρ‡Π°: Π½Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ счётчик, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΡ€ΠΈ Π½Π°ΠΆΠ°Ρ‚ΠΈΠΈ Π½Π° ΠΊΠ½ΠΎΠΏΠΊΡƒ Π±ΡƒΠ΄Π΅Ρ‚ ΡƒΠ²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Ρ‚ΡŒ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅.

Π’ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅ Π½ΠΈΠΆΠ΅ Π Π΅Π°ΠΊΡ‚ Π½Π΅ ΠΏΠΎΠ½ΠΈΠΌΠ°Π΅Ρ‚, Ρ‡Ρ‚ΠΎ Π½ΡƒΠΆΠ½ΠΎ ΠΎΠ±Π½ΠΎΠ²Π»ΡΡ‚ΡŒ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ Π² ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½ΠΎΠΌ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ ΠΌΡ‹ ΠΏΠΎΠ΄ΠΎΠ±Π½ΠΎΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠ΅ΠΉ отправляСм ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ значСния Π² JS (clg ΠΏΠΎΠΊΠ°ΠΆΠ΅Ρ‚, Ρ‡Ρ‚ΠΎ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ мСняСтся Π²Π½ΡƒΡ‚Ρ€ΠΈ JS), Π° Π½Π΅ Π² Π΄Π΅Ρ€Π΅Π²ΠΎ Π Π΅Π°ΠΊΡ‚Π°.

Нам Π½ΡƒΠΆΠ½ΠΎ Π±ΡƒΠ΄Π΅Ρ‚ Π²Ρ‹Π·Π²Π°Ρ‚ΡŒ Π² Ρ€Π΅Π°ΠΊΡ‚Π΅ ΠΏΠ΅Ρ€Π΅Ρ€Π΅Π½Π΄Π΅Ρ€ Π½ΡƒΠΆΠ½ΠΎΠ³ΠΎ Π½Π°ΠΌ значСния Π½Π° страницС.

Π₯ΡƒΠΊ useState() Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ с состояниСм ΠΈΒ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ для Π΅Π³ΠΎ обновлСния.

Во врСмя ΠΏΠ΅Ρ€Π²ΠΎΠ½Π°Ρ‡Π°Π»ΡŒΠ½ΠΎΠ³ΠΎ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π° Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌΠΎΠ΅ состояниС (state) совпадаСт со значСниСм, ΠΏΠ΅Ρ€Π΅Π΄Π°Π½Π½Ρ‹ΠΌ в качСствС ΠΏΠ΅Ρ€Π²ΠΎΠ³ΠΎ Π°Ρ€Π³ΡƒΠΌΠ΅Π½Ρ‚Π° (initialState).

Ѐункция setStateΒ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ для обновлСния состояния. Она ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ Π½ΠΎΠ²ΠΎΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ состояния и ставит Π²Β ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹ΠΉ Ρ€Π΅Π½Π΄Π΅Ρ€ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°.

export const State = (): JSX.Element => {
	let [counter, setCounter] = useState<number>(0);
 
	return (
		<div>
			<Link href={'/'}>
				<Button buttonType={'ghost'}>ΠžΠ±Ρ€Π°Ρ‚Π½ΠΎ</Button>
			</Link>
			<div className={styles.wrapper}>
				<h2 className={styles.title}>Π‘Ρ‡Ρ‘Ρ‚Ρ‡ΠΈΠΊ:</h2>
				<h1 className={styles.num}>{counter}</h1>
				<Button
					buttonType={'gray'}
					className={styles.reduce}
					onClick={() => setCounter(counter--)}
				>
					Π£ΠΌΠ΅Π½ΡŒΡˆΠΈΡ‚ΡŒ
				</Button>
				<Button
					buttonType={'purple'}
					className={styles.increase}
					onClick={() => setCounter(counter++)}
				>
					Π£Π²Π΅Π»ΠΈΡ‡ΠΈΡ‚ΡŒ
				</Button>
			</div>
		</div>
	);
};

ΠŸΡ€ΠΈ ΡƒΠ²Π΅Π»ΠΈΡ‡Π΅Π½ΠΈΠΈ значСния счётчика, число увСличиваСтся, Π° ΠΏΡ€ΠΈ ΡƒΠΌΠ΅Π½ΡŒΡˆΠ΅Π½ΠΈΠΈ - ΡƒΠΌΠ΅Π½ΡŒΡˆΠ°Π΅Ρ‚ΡΡ.

22:25 ➝ УправляСмый ΠΈΠ½ΠΏΡƒΡ‚

УправляСмый ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ - это ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚, Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ³ΠΎ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΈΠ·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ, ΠΈΠ·ΠΌΠ΅Π½ΠΈΠ² состояниС

export const ControlledInput = (): JSX.Element => {
	let [value, setValue] = useState<string>('Π—Π½Π°Ρ‡Π΅Π½ΠΈΠ΅');
 
	return (
		<div>
			<Link href={'/'}>
				<Button buttonType={'ghost'}>ΠžΠ±Ρ€Π°Ρ‚Π½ΠΎ</Button>
			</Link>
			<div className={styles.wrapper}>
				<h1>{value}</h1>
				<Input
					value={value}
					placeholder={'Пиши в мСня:)'}
					onChange={e => setValue(e.target.value)}
				/>
			</div>
		</div>
	);
};

ΠœΡ‹ связали состояниС <h1> с Ρ‚Π΅ΠΌ, Ρ‡Ρ‚ΠΎ находится Π² ΠΈΠ½ΠΏΡƒΡ‚Π΅

24:07 ➝ ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚

  • ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ ΠΌΡ‹ создаём Π² ΠΏΠ°ΠΏΠΊΠ΅ components
  • Π€Π°ΠΉΠ» ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° ΠΈ функция ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° всСгда ΠΈΠΌΠ΅Π½ΡƒΡŽΡ‚ΡΡ Π² PascalCase
  • ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ всСгда Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Ρ‚ΡŒ JSX.Element

И Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ Π΄Π°Π½Π½Ρ‹ΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ <Button> ΠΌΠΎΠΆΠ½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π² любом мСстС ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°. Π­Ρ‚ΠΈΡ… ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ² ΠΌΠΎΠΆΠ½ΠΎ Π½Π°Π²ΡΡ‚Π°Π²Π»ΡΡ‚ΡŒ сколько ΡƒΠ³ΠΎΠ΄Π½ΠΎ ΠΈ ΠΎΠ½ΠΈ Π±ΡƒΠ΄ΡƒΡ‚ нСзависимыми Π΄Ρ€ΡƒΠ³ ΠΎΡ‚ Π΄Ρ€ΡƒΠ³Π°

import { Button } from '@/components';  // ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ ΠΊΠ½ΠΎΠΏΠΊΡƒ
 
function Home() {
   return (
      <div className={styles.wrapper}>
         <h1>React Ρ„ΡƒΠ½Π΄Π°ΠΌΠ΅Π½Ρ‚Π°Π»ΡŒΠ½Ρ‹ΠΉ</h1>
         <div className={styles.links}>
            <Link href={'fundamentals/state'}>
               <Button buttonType={'purple'}>Бостояния React</Button>
            </Link>
            <Link href={'fundamentals/controlledInput'}>
	            {/* добавляСм ΠΊΠ½ΠΎΠΏΠΊΡƒ Π½Π° страницу */}
	            <Button buttonType={'purple'}>УправляСмый ΠΈΠ½ΠΏΡƒΡ‚</Button>
				<Button buttonType={'purple'}>УправляСмый ΠΈΠ½ΠΏΡƒΡ‚</Button>
				<Button buttonType={'purple'}>УправляСмый ΠΈΠ½ΠΏΡƒΡ‚</Button>
				<Button buttonType={'purple'}>УправляСмый ΠΈΠ½ΠΏΡƒΡ‚</Button>
				<Button buttonType={'purple'}>УправляСмый ΠΈΠ½ΠΏΡƒΡ‚</Button>
            </Link>
         </div>
      </div>
   );
}

26:40 ➝ ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ классовый ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚

ΠšΠ»Π°ΡΡΠΎΠ²Ρ‹ΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ Π²Π½ΡƒΡ‚Ρ€ΠΈ сСбя ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹

import React, { Component } from 'react';
import styles from './ClassCounter.module.scss';
import cn from 'classnames';
import { ClassCounterProps } from './ClassCounter.props';
import { Button } from '@/components';
 
export class ClassCounter extends Component<any, any> {
	constructor(props: ClassCounter) {
		super(props);
		this.state = {
			count: 0,
		};
 
		// здСсь ΠΌΡ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π²Π΅Ρ€Π½ΡƒΡ‚ΡŒ потСрянный контСкст выполнСния для ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠ²
		this.increment = this.increment.bind(this);
		this.decrement = this.decrement.bind(this);
	}
 
	increment(): void {
		this.setState({
			count: this.state.count + 1,
		});
	}
 
	decrement(): void {
		this.setState({
			count: this.state.count - 1,
		});
	}
 
	render() {
		return (
			<div>
				<h1>{this.state.count}</h1>
				<Button buttonType={'purple'} onClick={this.increment}>
					inc
				</Button>
				<Button buttonType={'gray'} onClick={this.decrement}>
					dec
				</Button>
			</div>
		);
	}
}

И Ρ‚Π°ΠΊ выглядит ΠΊΠ°ΡƒΠ½Ρ‚Π΅Ρ€:

Π‘ΠΎΠΊΡ€Π°Ρ‰Π΅Π½ΠΈΠ΅ ΠΏΡƒΡ‚Π΅ΠΉ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΎΠ² Π΄ΠΎ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ²

Π’Π°ΠΊ ΠΆΠ΅ Π½Ρƒ Π½ΡƒΠΆΠ½ΠΎ Π·Π°Π±Ρ‹Π²Π°Ρ‚ΡŒ, Ρ‡Ρ‚ΠΎ ΠΏΡ€ΠΈ создании ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° Π² ΠΏΠ°ΠΏΠΊΠ΅ components, ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΡΠΊΡΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΡƒΠ΄ΠΎΠ±Π½ΠΎ эти ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ ΠΈΠ· ΠΏΠ°ΠΏΠΊΠΈ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π² Π΄Ρ€ΡƒΠ³ΠΈΡ… ΠΏΠ°ΠΏΠΊΠ°Ρ… (pages ΠΈΠ»ΠΈ page-components)

components / index.ts

export * from './Button/Button';
export * from './Divider/Divider';
export * from './Input/Input';
export * from './Select/Select';
export * from './Paragraph/Paragraph';
export * from './ClassCounter/ClassCounter';
export * from './PostItem/PostItem';
export * from './PostForm/PostForm';
export * from './PostFilter/PostFilter';
export * from './PostList/PostList';

ПослС ΠΏΠΎΠ΄ΠΎΠ±Π½ΠΎΠ³ΠΎ экспорта, ΠΌΡ‹ смоТСм ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ доступ ΠΊ Π΄Π°Π½Π½Ρ‹ΠΌ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°ΠΌ, просто ΠΎΠ±Ρ€Π°Ρ‚ΠΈΠ²ΡˆΠΈΡΡŒ Ρ‡Π΅Ρ€Π΅Π·: import { Π½ΡƒΠΆΠ½Ρ‹ΠΉ_ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ } from './components'

30:25 ➝ Π§Ρ‚ΠΎ Ρ‚Π°ΠΊΠΎΠ΅ Ρ…ΡƒΠΊΠΈ? useState, useEffect

Π₯ΡƒΠΊ - это функция, ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ прСдоставляСт React для использования Π² Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹Ρ… ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°Ρ… ΠΈΠ»ΠΈ Π² своих собствСнных Ρ…ΡƒΠΊΠ°Ρ…

  • Π₯ΡƒΠΊΠΈ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽΡ‚ΡΡ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π½Π° Π²Π΅Ρ€Ρ…Π½Π΅ΠΌ ΡƒΡ€ΠΎΠ²Π½Π΅ влоТСнности. Π˜Ρ… нСльзя Π²ΠΊΠ»Π°Π΄Ρ‹Π²Π°Ρ‚ΡŒ Π² условия, Ρ†ΠΈΠΊΠ»Ρ‹ ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ.

31:10 ➝ Π‘Ρ‚ΠΈΠ»ΠΈ. CSS. ΠšΠ»Π°ΡΡΡ‹

Для наимСнования классов Π² React ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽΡ‚ Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚ className, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ слово class ΡƒΠΆΠ΅ Π·Π°Ρ€Π΅Π·Π΅Ρ€Π²ΠΈΡ€ΠΎΠ²Π°Π½ΠΎ ΠΏΠΎΠ΄ классы.

ΠœΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ просто ΠΈΠΌΠ΅Π½ΠΎΠ²Π°Ρ‚ΡŒ классы стилСй Π½Π°ΡˆΠΈΡ… элСмСнтов стандартным способом

А ΠΌΠΎΠΆΠ΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΌΠΎΠ΄ΡƒΠ»ΠΈ для описания стилСй. Π’ этом случаС Π½ΡƒΠΆΠ½ΠΎ:

  • Π’ Π½Π°Π·Π²Π°Π½ΠΈΠΈ Ρ„Π°ΠΉΠ»Π° стилСй ΡƒΠΊΠ°Π·Π°Ρ‚ΡŒ .module
  • Π’ className ΡƒΠΊΠ°Π·Π°Ρ‚ΡŒ класс Ρ‡Π΅Ρ€Π΅Π· Ρ‚ΠΎΡ‡ΠΊΡƒ ΠΎΡ‚ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Ρ… стилСй

Π’Π°ΠΊ ΠΆΠ΅ стили ΠΌΠΎΠΆΠ½ΠΎ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Ρ‡Π΅Ρ€Π΅Π· Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚ style, Π²Π½ΡƒΡ‚Ρ€ΡŒ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ³ΠΎ ΠΌΡ‹ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ со стилями

34:30 ➝ Props. АргумСнты ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°.

Бвойства, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ Π² ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚, Π½Π°Π·Ρ‹Π²Π°ΡŽΡ‚ΡΡ props

Π’ Ρ€Π°ΠΌΠΊΠ°Ρ… React ΠΏΡ€ΠΈ использовании Π΅Π³ΠΎ вмСстС с TS ΠΌΡ‹ обязаны ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ интСрфСйсы для Π½Π°ΡˆΠΈΡ… ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌΡ‹Ρ… пропсов (Ρ‡Ρ‚ΠΎΠ±Ρ‹ всСгда ΠΏΠΎΠ½ΠΈΠΌΠ°Ρ‚ΡŒ, Ρ‡Ρ‚ΠΎ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚).

Π˜Π½Ρ‚Π΅Ρ€Ρ„Π΅ΠΉΡΡ‹ для ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ² ΠΌΡ‹ ΠΎΠ±Ρ‹Ρ‡Π½ΠΎ Ρ€Π°ΡΡˆΠΈΡ€ΡΠ΅ΠΌ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ DetailedHTMLProps ΠΈ уточняСм, ΠΊΠ°ΠΊΠΈΠ΅ Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Ρ‹ ΠΎΠ½ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Ρ‚ΡŒ Ρ‡Π΅Ρ€Π΅Π· HTMLAttributes.

Π’ Π΄Π°Π½Π½ΠΎΠΌ случаС, ΠΌΡ‹ Ρ€Π°ΡΡˆΠΈΡ€ΡΠ΅ΠΌ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ ΠΎΡ‚ Π΄ΠΈΠ²Π°, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π±Ρ‹Π»Π° Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ Π² Π½Π΅Π³ΠΎ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ className.

Π’Π°ΠΊ ΠΆΠ΅ ΠΌΡ‹ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Π΅ΠΌ children с Ρ‚ΠΈΠΏΠΎΠΌ ReactNode - это Ρ‚Π΅ Π΄Π°Π½Π½Ρ‹Π΅, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ Π²ΠΊΠ»Π°Π΄Ρ‹Π²Π°Π΅ΠΌ ΠΌΠ΅ΠΆΠ΄Ρƒ ΠΎΡ‚ΠΊΡ€Ρ‹Π²Π°ΡŽΡ‰ΠΈΠΌ ΠΈ Π·Π°ΠΊΡ€Ρ‹Π²Π°ΡŽΡ‰ΠΈΠΌ Ρ‚Π΅Π³ΠΎΠΌ

PostItem.props.ts

import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
 
export interface PostItemProps
   extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
   children: ReactNode;
   title: string;
}

ВмСсто ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅ΠΌΠΎΠ³ΠΎ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Π° дСсутруктуризации { language, children } ΠΌΡ‹ Π±Ρ‹ ΠΌΠΎΠ³Π»ΠΈ просто Π½Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ props, Π½ΠΎ Π²Ρ‹Ρ‚Π°Ρ‰ΠΈΡ‚ΡŒ сразу Π½ΡƒΠΆΠ½Ρ‹Π΅ значСния - это самый ΠΎΠΏΡ‚ΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ способ взаимодСйствия, Ρ‡Ρ‚ΠΎΠ±Ρ‹ сразу Π²ΠΈΠ΄Π΅Ρ‚ΡŒ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌΡ‹Π΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹

Π’Π°ΠΊ ΠΆΠ΅, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ сразу всС ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Π΅ пропсы, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ Π²Π»ΠΎΠΆΠΈΠΌ Π² ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚, ΠΌΠΎΠΆΠ½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ...props ΠΏΡ€ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠΈ props ΠΈ Π² самом JSX ΡƒΠΊΠ°Π·Π°Ρ‚ΡŒ, Ρ‡Ρ‚ΠΎ ΠΌΡ‹ Π²Ρ‹ΠΊΠ»Π°Π΄Ρ‹Π²Π°Π΅ΠΌ всС пропсы Π² этот элСмСнт: <div {...props}>. Π’Π°ΠΊΠΎΠΉ ΠΏΠΎΠ΄Ρ…ΠΎΠ΄ Π±ΠΎΠ»Π΅Π΅ Π°ΠΊΡ‚ΡƒΠ°Π»Π΅Π½, ΠΊΠΎΠ³Π΄Π° ΠΌΡ‹ создаём свои ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ ΠΊΠ½ΠΎΠΏΠΎΠΊ, ΠΈΠ½ΠΏΡƒΡ‚ΠΎΠ² ΠΈ ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Ρ… простых элСмСнтов.

PostItem.tsx

export const PostItem = ({ title, children, className, ...props }: PostItemProps) => {
   return (
      <div className={cn(styles.wrapper, className)} {...props}>
         <div className={styles.post}>
            <div className={styles.post__content}>
               <h2>{title}</h2>
               <Paragraph size={'l'}>{children}</Paragraph>
            </div>
            <Button buttonType={'purple'} className={styles.post__button}>
               Π£Π΄Π°Π»ΠΈΡ‚ΡŒ пост
            </Button>
         </div>
      </div>
   );
};

ΠŸΠ΅Ρ€Π΅Π΄Π°ΡŽΡ‚ΡΡ пропсы Ρ€ΠΎΠ²Π½ΠΎ Ρ‚Π°ΠΊΠΈΠΌ ΠΆΠ΅ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ, ΠΊΠ°ΠΊ ΠΈ Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Ρ‹. Π’ случаС с TS компилятор Π½Π°ΠΌ подскаТСт, ΠΊΠ°ΠΊΠΈΠ΅ значСния ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ Π²ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ Π² Π΄Π°Π½Π½Ρ‹ΠΉ элСмСнт ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚

PostList.tsx

export const Posts = () => {
   const [postsData, setPostsData] = useState([
		{ id: 'asd1', title: 'Javascript', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
		{ id: 'adsgsa2', title: 'C#', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
		{ id: 'fsdagha3', title: 'Python', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
	]);
 
   return (
      <div>
         {postsData.map(p => (
            <PostItem key={p.id} title={p.title}>
               {p.body}
            </PostItem>
         ))}
      </div>
   );
};

Π’Π°ΠΊ ΠΆΠ΅ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ вывСсти props Π² консоль ΠΈ ΡƒΠ²ΠΈΠ΄ΠΈΠΌ, Ρ‡Ρ‚ΠΎ это просто ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ с Π΄Π°Π½Π½Ρ‹ΠΌΠΈ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ ΠΏΠ΅Ρ€Π΅Π΄Π°Π»ΠΈ Π² ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ ΠΈΠ·Π²Π½Π΅

36:55 ➝ Π Π°Π±ΠΎΡ‚Ρ‹ со списками. ΠŸΡ€Π΅ΠΎΠ±Ρ€Π°Π·ΠΎΠ²Π°Π½ΠΈΠ΅ массива ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠ² Π² массив React элСмСнтов

Когда Π½Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ вывСсти массив элСмСнтов с ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½ΠΎΠΉ структурой, ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ функциями JS Π²Π½ΡƒΡ‚Ρ€ΠΈ JSX. Для этого Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ Π½ΡƒΠΆΠ½ΠΎ Π²ΠΏΠΈΡΠ°Ρ‚ΡŒ Π²Π½ΡƒΡ‚Ρ€ΡŒ { } скобок.

Для ΠΏΠ΅Ρ€Π΅Π±ΠΎΡ€Π° массива ΠΌΠΎΠΆΠ½ΠΎ Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠ΅ΠΉ map().

Когда ΠΌΡ‹ создаём списки, ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ для всСх элСмСнтов Π½ΡƒΠΆΠ½ΠΎ ΡƒΠΊΠ°Π·Π°Ρ‚ΡŒ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡ ΠΈ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ Π΅Π³ΠΎ Ρ‡Π΅Ρ€Π΅Π· Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚ key. Для ΠΊΠ»ΡŽΡ‡Π° ΠΎΠ±Ρ‹Ρ‡Π½ΠΎ Π½Π΅ стоит ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ индСкс элСмСнта Π² массивС - это плохая ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠ°, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ ΠΎΠ½ ΠΌΠΎΠΆΠ΅Ρ‚ ΠΏΠΎΠΌΠ΅Π½ΡΡ‚ΡŒΡΡ послС измСнСния Ρ€Π°Π·ΠΌΠ΅Ρ€Π° массива. РСкомСндуСтся ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΊΠ°ΠΊΠΎΠΉ-Π»ΠΈΠ±ΠΎ статичный индСкс. Π­Ρ‚ΠΎ ΠΏΠΎΠΌΠΎΠΆΠ΅Ρ‚ Ρ€Π΅Π°ΠΊΡ‚Ρƒ Π·Π°ΠΏΠΎΠΌΠ½ΠΈΡ‚ΡŒ элСмСнт массива ΠΈ Π½Π΅ ΠΏΠ΅Ρ€Π΅Ρ€ΠΈΡΠΎΠ²Ρ‹Π²Π°Ρ‚ΡŒ всС Π²Ρ‹Π²Π΅Π΄Π΅Π½Π½Ρ‹Π΅ элСмСнты.

PostList.tsx

export const Posts = () => {
	const [postsData, setPostsData] = useState([
		{ id: 'asd1', title: 'Javascript', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
		{ id: 'adsgsa2', title: 'C#', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
		{ id: 'fsdagha3', title: 'Python', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
	]);
 
	return (
		<div>
			{postsData.map(p => (
				<PostItem key={p.id} title={p.title}>
					{p.body}
				</PostItem>
			))}
		</div>
	);
};

И ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π½ΠΎ Ρ‚Π°ΠΊ ΠΌΡ‹ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠΌ ΠΈΡ‚ΠΎΠ³ΠΎΠ²Ρ‹ΠΉ массив Π½Π°ΡˆΠΈΡ… элСмСнтов

42:30 ➝ Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ UI Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ. ΠŸΠ΅Ρ€Π²Ρ‹Π΅ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹. CSS ΠΌΠΎΠ΄ΡƒΠ»ΠΈ. ΠŸΡ€ΠΎΠΏΡ children

ΠžΠ±Ρ‹Ρ‡Π½ΠΎ Π² своСй Ρ€Π°Π±ΠΎΡ‚Π΅ придётся часто ΡΠΎΠ·Π΄Π°Π²Π°Ρ‚ΡŒ свою UI-Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΡƒ ΠΏΠΎΠ΄ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ сайт, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Ρ‹Π²Π°Ρ‚ΡŒ.

ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ ΠΊΠ½ΠΎΠΏΠΊΠΈ:

Button.tsx

import React from 'react';
import styles from './Button.module.scss';
import cn from 'classnames';
import { ButtonProps } from './Button.props';
 
export const Button = ({
	buttonType = 'gray',
	className,
	children,
	...props
}: ButtonProps): JSX.Element => {
	return (
		<button
			{/* Π² зависимости ΠΎΡ‚ ΠΏΠ΅Ρ€Π΅Π΄Π°Π½Π½ΠΎΠ³ΠΎ Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Π° стиля, Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΡ€ΠΈΡΠ²Π°ΠΈΠ²Π°Ρ‚ΡŒΡΡ свой ΡΡ‚ΠΈΠ»ΡŒ для ΠΊΠ½ΠΎΠΏΠΊΠΈ */}
			className={cn(styles.button, className, {
				[styles.gray]: buttonType == 'gray',
				[styles.ghost]: buttonType == 'ghost',
				[styles.purple]: buttonType == 'purple',
			})}
 
		{/* сюда Π±ΡƒΠ΄ΡƒΡ‚ ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒΡΡ всС ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Π΅ пропсы, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ ΠΏΡ€ΠΈΠΏΠΈΡˆΠ΅ΠΌ ΠΊ ΠΊΠ½ΠΎΠΏΠΊΠ΅ */}
			{...props}
		>
			{/* Ρ‚ΡƒΡ‚ Π±ΡƒΠ΄Π΅Ρ‚ Π½Π°Ρ…ΠΎΠ΄ΠΈΡ‚ΡŒΡΡ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΏΠ΅Ρ€Π΅Π΄Π°Π½Π½ΠΎΠ΅ ΠΌΠ΅ΠΆΠ΄Ρƒ Ρ‚Π΅Π³Π°ΠΌΠΈ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° */}
			{children}
		</button>
	);
};

Π’ΡƒΡ‚ ΠΌΡ‹ опишСм Ρ‚Π΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π΄ΠΎΠ»ΠΆΠ½Π° ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Ρ‚ΡŒ Π² сСбя ΠΊΠ½ΠΎΠΏΠΊΠ°. Π§Ρ‚ΠΎΠ±Ρ‹ ΠΎΠΏΠΈΡΠ°Ρ‚ΡŒ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌΡ‹Π΅ Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Ρ‹, ΠΌΡ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Ρ€Π°ΡΡˆΠΈΡ€ΠΈΡ‚ΡŒ интСрфСйс ΠΎΡ‚ DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, Π³Π΄Π΅ ΠΌΡ‹ Ρ€Π°ΡΡˆΠΈΡ€ΡΠ΅ΠΌΡΡ ΠΎΡ‚ Button.

Button.props.ts

import { ButtonHTMLAttributes, DetailedHTMLProps, ReactNode } from 'react';
 
export interface ButtonProps
	extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
	children: ReactNode;
	buttonType: 'gray' | 'purple' | 'ghost';
}

Π‘Ρ‚ΠΈΠ»ΠΈ ΠΊΠ½ΠΎΠΏΠΊΠΈ:

Button.module.css

.button {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 10px;
 
	padding: 20px;
 
	width: 100%;
 
	border: none;
	border-radius: 8px;
 
	font-size: 16px;
	font-weight: 700;
 
	color: white;
 
	cursor: pointer;
}
 
.gray {
	grid-area: reduce;
	background: var(--anti-accent);
	transition: all 0.2s;
 
	&:hover {
		background: var(--anti-accent-hover);
		transform: translateY(-4px);
	}
 
	&:active {
		background: var(--anti-accent-clicked);
		transform: translateY(4px);
	}
}
 
.purple {
	grid-area: increase;
	background: var(--primary);
	transition: all 0.2s;
 
	&:hover {
		background: var(--primary-hover);
		transform: translateY(-4px);
	}
 
	&:active {
		background: var(--primary-clicked);
		transform: translateY(4px);
	}
}
 
.ghost {
	position: absolute;
	top: 20px;
	left: 20px;
 
	width: 100px;
	height: 20px;
 
	grid-area: increase;
	background: none;
	transition: all 0.2s;
 
	border: 2px dashed var(--anti-accent);
	border-radius: 2px;
 
	&:hover {
		background: var(--anti-accent-hover);
		transform: translateY(-4px);
	}
 
	&:active {
		transform: translateY(4px);
	}
}

50:00 ➝ ΠŸΡ€Π΅Π΄ΠΎΡ‚Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ страницы ΠΏΡ€ΠΈ submit Ρ„ΠΎΡ€ΠΌΡ‹

Π§Ρ‚ΠΎΠ±Ρ‹ ΠΏΡ€Π΅Π΄ΠΎΡ‚Π²Ρ€Π°Ρ‚ΠΈΡ‚ΡŒ срабатываниС Π΄Π΅Ρ„ΠΎΠ»Ρ‚Π½ΠΎΠΉ ΠΏΠ΅Ρ€Π΅Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ страницы, Π½ΡƒΠΆΠ½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π½Π° ΠΈΠ²Π΅Π½Ρ‚Π΅ Π΄Π°Π½Π½ΠΎΠ³ΠΎ элСмСнта ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ повСдСния Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π° preventDefault()

const addNewPost = (event): void => {
   event.preventDefault();
};

50:45 ➝ Ρ…ΡƒΠΊ useRef. Доступ ΠΊ DOM элСмСнту. НСуправляСмый ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚

НиТС ΠΏΡ€ΠΈΠ²Π΅Π΄Π΅Π½Ρ‹ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ управляСмого ΠΈ нСуправляСмого ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°:

  1. УправляСмый:
  • УправляСмый ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ ΠΈΠΌΠ΅Π΅Ρ‚ ΠΏΠΎΠ΄ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒΠ½ΠΎΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅
  • Π­Ρ‚ΠΎ состояниС связано с ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠΌ Ρ‡Π΅Ρ€Π΅Π· Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅
  1. НСуправляСмый ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚:
  • НС ΠΈΠΌΠ΅Π΅Ρ‚ ΠΏΠΎΠ΄ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒΠ½ΠΎΠ³ΠΎ значСния
  • Для получСния доступа ΠΊ Π½Π΅ΠΌΡƒ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ Ρ…ΡƒΠΊ useRef

ΠŸΠΎΡ€ΡΠ΄ΠΎΠΊ использования Ρ€Π΅Ρ„Π°:

  • Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅ΠΌ useRef
  • ΠŸΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ Π² Π°Ρ‚Ρ€ΠΈΡŒΠ±ΡƒΡ‚ ref ΠΏΡ€ΠΎΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ useRef
  • ΠžΠ±ΠΎΡ€Π°Ρ‡ΠΈΠ²Π°Π΅ΠΌ сам ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Π² своСй Π²Π½ΡƒΡ‚Ρ€Π΅Π½Π½Π΅ΠΉ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ Π² forwardRef
  • ΠŸΡ€ΠΎΠΊΠΈΠ΄Ρ‹Π²Π°Π΅ΠΌ ref Π²Π½ΡƒΡ‚Ρ€ΡŒ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°
export const PostList = () => {
	const [postsData, setPostsData] = useState([
		{ id: 'asd1', title: 'Javascript', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
		{ id: 'adsgsa2', title: 'C#', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
		{ id: 'fsdagha3', title: 'Python', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
	]);
 
	const [title, setTitle] = useState<string>('');
 
	// Ρ…ΡƒΠΊ рСфСрСнса Π½Π° ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ DOM-Π΄Π΅Ρ€Π΅Π²Π°
	const bodyInputRef = useRef<HTMLInputElement>(null);
 
	const addNewPost = (event: any): void => {
		event.preventDefault();
 
		// Π²Ρ‹Π²ΠΎΠ΄ΠΈΠΌ Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΉ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚, Ссли ΠΎΠ½ Π΅ΡΡ‚ΡŒ ?
		console.log(bodyInputRef.current?.value);
	};
 
	return (
		<div className={styles.wrapper}>
			<div className={styles.formBlock}>
				<form className={styles.form}>
 
 
 
 
					{/* управляСмый ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ */}
					<Input
						value={title}
						onChange={e => setTitle(e.target.value)}
						className={styles.form__input}
						type='text'
						placeholder={'НазваниС поста'}
					/>
					{/* нСуправляСмый ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ */}
					<Input
						// навСшиваСм ссылку Ρ€Π΅Ρ„Π°
						ref={bodyInputRef}
						className={styles.form__input}
						type='text'
						placeholder={'ОписаниС поста'}
					/>
 
 
 
 
					<Button
						className={styles.form__button}
						buttonType={'purple'}
						onClick={addNewPost}
					>
						Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ пост
					</Button>
				</form>
			</div>
			<div className={styles.list}>
				{postsData.map(p => (
					<PostItem key={p.id} title={p.title}>
						{p.body}
					</PostItem>
				))}
			</div>
		</div>
	);
};

Π”Π°Π»Π΅Π΅ сам ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ <Input> Π½ΡƒΠΆΠ½ΠΎ ΠΎΠ±Π΅Ρ€Π½ΡƒΡ‚ΡŒ Π² forwardRef ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ (всю Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ ΠΎΠ±Π΅Ρ€Π½ΡƒΡ‚ΡŒ Π²Π½ΡƒΡ‚Ρ€ΡŒ ( ) скобок) ΠΈ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ Π²Π½ΡƒΡ‚Ρ€ΡŒ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ΅ свойство ref. Π‘Π°ΠΌΠΎ свойство ref Π½ΡƒΠΆΠ½ΠΎ Π²Π»ΠΎΠΆΠΈΡ‚ΡŒ Π² качСствС Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Π° Π²Π½ΡƒΡ‚Ρ€ΡŒ нашСго ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°

Input.tsx

// ΠΎΠ±ΠΎΡ€Π°Ρ‡ΠΈΠ²Π°Π΅ΠΌ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ Π² forwardRef ΠΈ Π²ΠΊΠ»Π°Π΄Ρ‹Π²Π°Π΅ΠΌ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ ref
export const Input = forwardRef<HTMLInputElement, InputProps>(
	({ className, ...props }: InputProps, ref: ForwardedRef<HTMLInputElement>): JSX.Element => {
		// здСсь ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ Π² качСствС Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Π° ссылку Ρ€Π΅Ρ„Π° ref={ref}
		return <input ref={ref} className={cn(className, styles.input)} {...props} />;
	},
);
 

И ΠΏΠΎ ΠΈΡ‚ΠΎΠ³Ρƒ, ΠΌΡ‹ смоТСм ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ Π² консоли, Ρ‡Ρ‚ΠΎ Ρ€Π΅Ρ„ Π΄Π°Ρ‘Ρ‚ Π½Π°ΠΌ доступ ΠΊ Π·Π½Π°Ρ‡Π΅Π½ΠΈΡŽ Π΄Π°Π½Π½ΠΎΠ³ΠΎ ΠΈΠ½ΠΏΡƒΡ‚Π°

И поэтому Π²Π΅Ρ€Π½Ρ‘ΠΌ ΠΎΠ±Ρ€Π°Ρ‚Π½ΠΎ ΠΎΠ±Ρ‹Ρ‡Π½ΠΎΠ΅ взаимодСйствиС с Π΄Π΅Ρ€Π΅Π²ΠΎΠΌ. Однако Ρ‚ΡƒΡ‚ сохраняСтся ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠ° Π² Ρ‚ΠΎΠΌ, Ρ‡Ρ‚ΠΎ ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Π΄Π²Π° Ρ€Π°Π·Π½Ρ‹Ρ… useState, хотя ΠΌΠΎΠ³Π»ΠΈ Π±Ρ‹ ΡΠΎΠΊΡ€Π°Ρ‚ΠΈΡ‚ΡŒ запись

export const PostList = () => {
	const [postsData, setPostsData] = useState([
		{ id: 'asd1', title: 'Javascript', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
		{ id: 'adsgsa2', title: 'C#', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
		{ id: 'fsdagha3', title: 'Python', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
	]);
 
	const [title, setTitle] = useState<string>('');
	const [body, setBody] = useState<string>('');
 
	const addNewPost = (event: any): void => {
		event.preventDefault();
 
		// ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠ΅ значСния ΠΈΠ· useState
		const newPost = {
			id: `${Date.now()}`,
			title,
			body,
		};
 
		// Π½Π΅ ΠΌΡƒΡ‚ΠΈΡ€ΡƒΠ΅ΠΌ массив - вставляСм старый массив ΠΈ добавляСм Π½ΠΎΠ²Ρ‹ΠΉ элСмСнт
		setPostsData([...postsData, newPost]);
 
		// ΠžΡ‡ΠΈΡ‰Π°Π΅ΠΌ ΠΈΠ½ΠΏΡƒΡ‚Ρ‹
		setTitle('');
		setBody('');
	};
 
	return (
		<div className={styles.wrapper}>
			<div className={styles.formBlock}>
				<form className={styles.form}>
					<Input
						value={title}
						onChange={e => setTitle(e.target.value)}
						className={styles.form__input}
						type='text'
						placeholder={'НазваниС поста'}
					/>
					<Input
						value={body}
						onChange={e => setBody(e.target.value)}
						className={styles.form__input}
						type='text'
						placeholder={'ОписаниС поста'}
					/>
					<Button
						className={styles.form__button}
						buttonType={'purple'}
						onClick={addNewPost}
					>
						Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ пост
					</Button>
				</form>
			</div>
			<div className={styles.list}>
				{postsData.map(p => (
					<PostItem key={p.id} title={p.title}>
						{p.body}
					</PostItem>
				))}
			</div>
		</div>
	);
};

Π’ΡƒΡ‚ прСдставлСна Π±ΠΎΠ»Π΅Π΅ лаконичная запись с использованиСм ΠΎΠ΄Π½ΠΎΠ³ΠΎ useState ΠΈ сокращённой Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠ΅ΠΉ addNewPost

import React, { useRef, useState } from 'react';
import styles from './PostList.module.scss';
import { PostItem } from '@/components/PostItem/PostItem';
import { Input } from '@/components/Input/Input';
import { Button } from '@/components';
 
export const PostList = () => {
	const [postsData, setPostsData] = useState([
		{ id: 'asd1', title: 'Javascript', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
		{ id: 'adsgsa2', title: 'C#', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
		{ id: 'fsdagha3', title: 'Python', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
	]);
 
	const [post, setPost] = useState<{ title: string; body: string }>({
		title: '',
		body: '',
	});
 
	const addNewPost = (event: any): void => {
		event.preventDefault();
 
		// добавляСм Π½ΠΎΠ²ΠΎΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅
		setPostsData([...postsData, { ...post, id: `${Date.now()}` }]);
 
		// ΠΎΡ‡ΠΈΡ‰Π°Π΅ΠΌ ΠΈΠ½ΠΏΡƒΡ‚Ρ‹
		setPost({
			title: '',
			body: '',
		});
	};
 
	return (
		<div className={styles.wrapper}>
			<div className={styles.formBlock}>
				<form className={styles.form}>
					<Input
						value={post.title}
						// сюда Π·Π°ΠΊΠΈΠ΄Ρ‹Π²Π°Π΅ΠΌ старый пост ΠΈ ΠΏΠ΅Ρ€Π΅Π·Π°Ρ‚ΠΈΡ€Π°Π΅ΠΌ Π½ΡƒΠΆΠ½ΠΎΠ΅ ΠΏΠΎΠ»Π΅
						onChange={e => setPost({ ...post, title: e.target.value })}
						className={styles.form__input}
						type='text'
						placeholder={'НазваниС поста'}
					/>
					<Input
						value={post.body}
						// сюда Π·Π°ΠΊΠΈΠ΄Ρ‹Π²Π°Π΅ΠΌ старый пост ΠΈ ΠΏΠ΅Ρ€Π΅Π·Π°Ρ‚ΠΈΡ€Π°Π΅ΠΌ Π½ΡƒΠΆΠ½ΠΎΠ΅ ΠΏΠΎΠ»Π΅
						onChange={e => setPost({ ...post, body: e.target.value })}
						className={styles.form__input}
						type='text'
						placeholder={'ОписаниС поста'}
					/>
					<Button
						className={styles.form__button}
						buttonType={'purple'}
						onClick={addNewPost}
					>
						Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ пост
					</Button>
				</form>
			</div>
			<div className={styles.list}>
				{postsData.map(p => (
					<PostItem key={p.id} title={p.title}>
						{p.body}
					</PostItem>
				))}
			</div>
		</div>
	);
};

Π˜Ρ‚ΠΎΠ³: Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ Π½ΠΎΠ²ΠΎΠ³ΠΎ поста Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚

57:35 ➝ React Devtools. Π˜Π½ΡΡ‚Ρ€ΡƒΠΌΠ΅Π½Ρ‚Ρ‹ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ° React

React DevTools - Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹ΠΉ ΠΏΠ»Π°Π³ΠΈΠ½ Π² Ρ€Π°Π±ΠΎΡ‚Π΅, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΠΎΠ·Π²ΠΎΠ»ΠΈΡ‚ ΠΏΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Π΄Π΅Ρ€Π΅Π²ΠΎ элСмСнтов страницы, влияниС измСнСния стСйта ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ²

59:15 ➝ ОбмСн Π΄Π°Π½Π½Ρ‹ΠΌΠΈ ΠΌΠ΅ΠΆΠ΄Ρƒ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°ΠΌΠΈ. ΠžΡ‚ родитСля ΠΊ Ρ€Π΅Π±Π΅Π½ΠΊΡƒ. ΠžΡ‚ Ρ€Π΅Π±Π΅Π½ΠΊΠ° ΠΊ Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŽ.

ΠœΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ Ρ‡Π΅Ρ‚Ρ‹Ρ€ΡŒΠΌΡ Ρ€Π°Π·Π½Ρ‹ΠΌΠΈ способами:

  • Π‘Π°ΠΌΡ‹ΠΉ простой стандартный - это ΠΎΡ‚ родитСля ΠΊ Ρ€Π΅Π±Ρ‘Π½ΠΊΡƒ
  • ΠžΡ‚ Ρ€Π΅Π±Ρ‘Π½ΠΊΠ° ΠΊ Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŽ выполняСтся Ρ‡Π΅Ρ€Π΅Π· callback-Ρ„ΡƒΠΊΠ½Ρ†ΠΈΡŽ
  • ΠœΠ΅ΠΆΠ΄Ρƒ Π΄ΠΎΡ‡Π΅Ρ€Π½ΠΈΠΌΠΈ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°ΠΌΠΈ (Ρ‡Π΅Ρ€Π΅Π· Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΈΠΉ)
  • И глобально Π² Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹Π΅ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° (Π·Π°Ρ‡Π°ΡΡ‚ΡƒΡŽ Ρ‡Π΅Ρ€Π΅Π· контСкст)

ΠŸΠ΅Ρ€Π²Ρ‹ΠΌ Π΄Π΅Π»ΠΎΠΌ, Π½ΡƒΠΆΠ½ΠΎ Π²Ρ‹Π΄Π΅Π»ΠΈΡ‚ΡŒ Ρ„ΠΎΡ€ΠΌΡƒ добавлСния Π½ΠΎΠ²ΠΎΠ³ΠΎ поста Π² ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚. И Ρ‚ΡƒΡ‚ Π½Π°ΠΌ понадобится Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡Ρƒ пропсов ΠΎΡ‚ Π΄ΠΎΡ‡Π΅Ρ€Π½Π΅Π³ΠΎ элСмСнта ΠΊ Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΎΠΌΡƒ.

ΠŸΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ Ρ‡Π΅Ρ€Π΅Π· create={createPost} Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ ΠΎΡ‚ Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΎΠ³ΠΎ элСмСнта ΠΊ Π΄ΠΎΡ‡Π΅Ρ€Π½Π΅ΠΉ Ρ„ΠΎΡ€ΠΌΠ΅ Π½Π° Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ поста.

И ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ remove={removePost} для удалСния поста, Π½ΠΎ ΡƒΠΆΠ΅ нСпосрСдствСнно Π² Π°ΠΉΡ‚Π΅ΠΌ поста

PostList.tsx

export const PostList = () => {
   const [postsData, setPostsData] = useState<IPost[]>([
		{ id: 'asd1', title: 'Javascript', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
		{ id: 'adsgsa2', title: 'C#', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
		{ id: 'fsdagha3', title: 'Python', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
	]);
 
   // коллбэк функция для создания поста, ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ Π² Π΄ΠΎΡ‡Π΅Ρ€Π½ΠΈΠΉ элСмСнт
   const createPost = (newPost: IPost): void => {
      setPostsData([...postsData, newPost]);
   };
 
   // коллбэк функция для удалСния поста, ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ Π² Π΄ΠΎΡ‡Π΅Ρ€Π½ΠΈΠΉ элСмСнт
   const removePost = (post: IPost): void => {
      // Π² стСйт Π²Π΅Ρ€Π½Ρ‘ΠΌ Π½ΠΎΠ²Ρ‹ΠΉ массив, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‚Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ΠΎΠ²Π°Π½ Ρ‡Π΅Ρ€Π΅Π· filter
      setPostsData(postsData.filter(p => p.id !== post.id));
   };
 
   return (
      <div className={styles.wrapper}>
         <PostForm create={createPost} />
         <div className={styles.list}>
            {postsData.map(p => (
               <PostItem remove={removePost} key={p.id} post={p} />
            ))}
         </div>
      </div>
   );
};

Π’ΡƒΡ‚ прСдставлСн интСрфСйс поста Π² ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠΌ Ρ„Π°ΠΉΠ»Π΅

PostList.interface.ts

export interface IPost {
	id: string;
	title: string;
	body: string;
}

БСйчас Π½ΡƒΠΆΠ½ΠΎ Ρ€Π°Π·Π±ΠΈΡ‚ΡŒ Π»ΠΎΠ³ΠΈΠΊΡƒ Ρ‚Π°ΠΊ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΌΠΎΠΆΠ½ΠΎ Π±Ρ‹Π»ΠΎ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ Π²Π½ΡƒΡ‚Ρ€ΡŒ Π΄ΠΎΡ‡Π΅Ρ€Π½Π΅Π³ΠΎ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ ΠΎΡ‚ Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΎΠ³ΠΎ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°

PostForm.props.ts

import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
import { IPost } from '@/page-components/PostList/PostList.interface';
 
export interface PostFormProps
	extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
	create: (newPost: IPost) => void;
}

Π£ΠΆΠ΅ сам ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ элСмСнт Π±ΡƒΠ΄Π΅Ρ‚ Π² сСбя ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Ρ‚ΡŒ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ удалСния поста ΠΏΠΎ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½ΠΎΠΌΡƒ посту ΠΈ сам пост

PostItem.props.ts

import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
import { IPost } from '@/page-components/PostList/PostList.interface';
 
export interface PostItemProps
	extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
	post: IPost;
	remove: (post: IPost) => void;
}

Π”Π°Π»Π΅Π΅ Ρ‚ΡƒΡ‚ Π²Ρ‹Π·Π²Π°Π΅ΠΌ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ ΠΈΠ· Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΎΠ³ΠΎ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° create() Π² Π΄ΠΎΡ‡Π΅Ρ€Π½Π΅ΠΌ элСмСнтС

PostForm.tsx

import React, { useState } from 'react';
import styles from './PostForm.module.scss';
import { Input } from '@/components/Input/Input';
import { Button } from '@/components';
import { PostFormProps } from '@/components/PostForm/PostForm.props';
 
export const PostForm = ({ create }: PostFormProps) => {
	const [post, setPost] = useState<{ title: string; body: string }>({
		title: '',
		body: '',
	});
 
	const addNewPost = (event: any): void => {
		event.preventDefault();
 
		const newPost = {
			...post,
			id: `${Date.now()}`,
		};
 
		// Π²Ρ‹Π·Ρ‹Π²Π°Π΅ΠΌ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ родитСля, Π² ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ Π½ΠΎΠ²Ρ‹ΠΉ пост
		create(newPost);
 
		setPost({
			title: '',
			body: '',
		});
	};
 
	return (
		<div className={styles.formBlock}>
			<form className={styles.form}>
				<Input
					value={post.title}
					onChange={e => setPost({ ...post, title: e.target.value })}
					className={styles.form__input}
					type='text'
					placeholder={'НазваниС поста'}
				/>
				<Input
					value={post.body}
					onChange={e => setPost({ ...post, body: e.target.value })}
					className={styles.form__input}
					type='text'
					placeholder={'ОписаниС поста'}
				/>
				<Button className={styles.form__button} buttonType={'purple'} onClick={addNewPost}>
					Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ пост
				</Button>
			</form>
		</div>
	);
};

Π”Π°Π»Π΅Π΅ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ΠΈΠΌ Π² элСмСнт ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ поста. Π’ΡƒΡ‚ ΠΊΠ½ΠΎΠΏΠΊΠΎΠΉ Π²Ρ‹Π·Ρ‹Π²Π°Π΅ΠΌ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ удалСния поста, пСрСдавая Π² Π½Π΅Ρ‘ ΠΏΠΎΠ»Π½Ρ‹ΠΉ пост: () => remove(post)

PostItem.tsx

export const PostItem = ({ remove, post, className, ...props }: PostItemProps) => {
	return (
		<div className={cn(styles.wrapper, className)} {...props}>
			<div className={styles.post}>
				<div className={styles.post__content}>
					<h2>{post.title}</h2>
					<Paragraph size={'l'}>{post.body}</Paragraph>
				</div>
				<Button
					onClick={() => remove(post)}
					buttonType={'purple'}
					className={styles.post__button}
				>
					Π£Π΄Π°Π»ΠΈΡ‚ΡŒ пост
				</Button>
			</div>
		</div>
	);
};

НовыС посты всё Ρ‚Π°ΠΊ ΠΆΠ΅ Π΄ΠΎΠ±Π°Π²Π»ΡΡŽΡ‚ΡΡ!

Π£Π΄Π°Π»Π΅Π½ΠΈΠ΅ поста Ρ‚Π°ΠΊ ΠΆΠ΅ Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚:

01:04:20 ➝ ΠžΡ‚Ρ€ΠΈΡΠΎΠ²ΠΊΠ° ΠΏΠΎ ΡƒΡΠ»ΠΎΠ²ΠΈΡŽ

ΠžΡ‚Ρ€ΠΈΡΠΎΠ²ΠΊΠ° ΠΏΠΎ ΡƒΡΠ»ΠΎΠ²ΠΈΡŽ выполняСтся ΠΊΡ€Π°ΠΉΠ½Π΅ просто - Ρ‡Π΅Ρ€Π΅Π· Ρ‚Π΅Ρ€Π½Π°Ρ€Π½Ρ‹ΠΉ ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€:

PostList.tsx

return (
	<div className={styles.wrapper}>
		<PostForm create={createPost} />
		<div className={styles.list}>
			{postsData.length ? (
				postsData.map(p => <PostItem remove={removePost} key={p.id} post={p} />)
			) : (
				<h2 style={{ textAlign: 'center' }}>ΠŸΠΎΡΡ‚Ρ‹ Π½Π΅ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Ρ‹</h2>
			)}
		</div>
	</div>
);

ΠŸΡ€ΠΈ ΡƒΠ΄Π°Π»Π΅Π½ΠΈΠΈ всСх постов, Ρƒ нас Π²Ρ‹Π»Π΅Π·Π΅Ρ‚ надпись:

01:05:30 ➝ Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ°. Π’Ρ‹ΠΏΠ°Π΄Π°ΡŽΡ‰ΠΈΠΉ список

ΠŸΠ΅Ρ€Π²Ρ‹ΠΌ Π΄Π΅Π»ΠΎΠΌ, Π½ΡƒΠΆΠ½ΠΎ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ сортировки. Он Π±ΡƒΠ΄Π΅Ρ‚ Π² сСбя ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Ρ‚ΡŒ массив ΠΎΠΏΡ†ΠΈΠΉ, Π΄Π΅Ρ„ΠΎΠ»Ρ‚Π½ΡƒΡŽ ΠΎΠΏΡ†ΠΈΡŽ ΠΈ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½ΠΎΠ΅ Π²Ρ‹Π±Ρ€Π°Π½Π½ΠΎΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΌ

Select.props.ts

import { DetailedHTMLProps, HTMLAttributes } from 'react';
 
export interface SelectProps
   extends DetailedHTMLProps<HTMLAttributes<HTMLSelectElement>, HTMLSelectElement> {
   options: { value: string; name: string }[];
   defaultValue: string;
   value: string;
}

ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ сСлСкта Π²Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ массив ΠΏΠ΅Ρ€Π΅Π΄Π°Π½Π½Ρ‹Ρ… Π² Π½Π΅Π³ΠΎ ΠΎΠΏΡ†ΠΈΠΉ ΠΈ Ρ‚Ρ€ΠΈΠ³Π³Π΅Ρ€ΠΈΡ‚ ΠΏΠ΅Ρ€Π΅Π΄Π°Π½Π½ΡƒΡŽ Π² Π½Π΅Π³ΠΎ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ onChange ΠΏΡ€ΠΈ Π²Ρ‹Π±ΠΎΡ€Π΅ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½ΠΎΠ³ΠΎ сСлСкта

Select.tsx

import React from 'react';
import cn from 'classnames';
import styles from './Select.module.scss';
import { SelectProps } from '@/components/Select/Select.props';
 
export const Select = ({
   defaultValue,
   options,
   className,
   value,
   onChange,
   ...props
}: SelectProps): JSX.Element => {
   return (
      <select
         value={value}
         onChange={event => onChange(event.target.value)}
         className={styles.select}
      >
	    <option disabled value=''>
            {defaultValue}
		</option>
         {options.map(option => (
            <option key={option.value} value={option.value}>
               {option.name}
            </option>
         ))}
      </select>
   );
};

Π’ Π³Π»Π°Π²Π½ΠΎΠΌ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅ ΠΌΡ‹ создали Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ sortPosts, ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ ΠΈ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ Π² Π΄ΠΎΡ‡Π΅Ρ€Π½ΠΈΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ сСлСкта. Π’Π½ΡƒΡ‚Ρ€ΠΈ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ ΠΌΡ‹ устанавливаСм Ρ‚ΠΈΠΏ сСлСкшСна setSelectedSort ΠΈ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΠΌ сравнСниС списков Ρ‡Π΅Ρ€Π΅Π· localeCompare.

PostList.tsx

export const PostList = () => {
   const [postsData, setPostsData] = useState<IPost[]>([
      { id: 'asd1', title: 'Javascript', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
      { id: 'adsgsa2', title: 'C#', body: 'Π₯Ρ€ΠΎΡˆΠΈΠΉ язык' },
      { id: 'fsdagha3', title: 'Python', body: 'ΠŸΠΎΡ‡Π΅ΠΌΡƒ Π±Ρ‹ ΠΈ Π½Π΅Ρ‚?' },
   ]);
 
   // состояниС для элСмСнта сортировки select
   const [selectedSort, setSelectedSort] = useState<'title' | 'body' | ''>('');
 
   // коллбэк функция для создания поста, ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ Π² Π΄ΠΎΡ‡Π΅Ρ€Π½ΠΈΠΉ элСмСнт
   const createPost = (newPost: IPost): void => {
      setPostsData([...postsData, newPost]);
   };
 
   // коллбэк функция для удалСния поста, ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ Π² Π΄ΠΎΡ‡Π΅Ρ€Π½ΠΈΠΉ элСмСнт
   const removePost = (post: IPost): void => {
      // Π² стСйт Π²Π΅Ρ€Π½Ρ‘ΠΌ Π½ΠΎΠ²Ρ‹ΠΉ массив, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‚Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ΠΎΠ²Π°Π½ Ρ‡Π΅Ρ€Π΅Π· filter
      setPostsData(postsData.filter(p => p.id !== post.id));
   };
 
   // функция для сортировки постов
   const sortPosts = (sort: 'title' | 'body'): void => {
      setSelectedSort(sort);
 
      // Ρ‚ΡƒΡ‚ ΠΌΡ‹ сортируСм массив, Π½Π΅ мутируя Π΅Π³ΠΎ состояниС Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ
      setPostsData([...postsData].sort((a, b) => a[sort].localeCompare(b[sort])));
   };
 
   return (
      <div className={styles.wrapper}>
         <PostForm create={createPost} />
 
         <Divider />
         <Select
            value={selectedSort}
            onChange={sortPosts}
            defaultValue={'Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ°'}
            options={[
               { value: 'title', name: 'По Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΡƒ' },
               { value: 'body', name: 'По описанию' },
            ]}
         />
 
         <div className={styles.list}>
            {postsData.length ? (
               postsData.map(p => <PostItem remove={removePost} key={p.id} post={p} />)
            ) : (
               <h2 style={{ textAlign: 'center' }}>ΠŸΠΎΡΡ‚Ρ‹ Π½Π΅ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Ρ‹</h2>
            )}
         </div>
      </div>
   );
};

Оба Π²ΠΈΠ΄Π° сортировки:

01:15:10 ➝ useMemo. ΠœΠ΅ΠΌΠΎΠΈΠ·Π°Ρ†ΠΈΡ. ΠšΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅. Поиск. Π€ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΡ.

Π₯ΡƒΠΊ useMemo Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ ΠΌΠ΅ΠΌΠΎΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅.

ΠŸΠ΅Ρ€Π²Ρ‹ΠΌ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ΠΎΠΌ Ρ…ΡƒΠΊ ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ Π² сСбя Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ, которая высчитываСт ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½ΠΎΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅. Π’Ρ‚ΠΎΡ€Ρ‹ΠΌ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ΠΎΠΌ ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ Π² сСбя массив зависимостСй. Если какая-Π»ΠΈΠ±ΠΎ ΠΈΠ· зависимостСй измСнилась, Ρ‚ΠΎ Ρ…ΡƒΠΊ Π·Π°Π½ΠΎΠ²ΠΎ пСрСсчитываСт Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅.

Если массив зависимостСй Π½Π΅Β Π±Ρ‹Π» ΠΏΠ΅Ρ€Π΅Π΄Π°Π½, Π½ΠΎΠ²ΠΎΠ΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ Π±ΡƒΠ΄Π΅Ρ‚ Π²Ρ‹Ρ‡ΠΈΡΠ»ΡΡ‚ΡŒΡΡ ΠΏΡ€ΠΈ ΠΊΠ°ΠΆΠ΄ΠΎΠΌ Ρ€Π΅Π½Π΄Π΅Ρ€Π΅.

Π­Ρ‚Π° оптимизация ΠΏΠΎΠΌΠΎΠ³Π°Π΅Ρ‚ ΠΈΠ·Π±Π΅ΠΆΠ°Ρ‚ΡŒ дорогостоящих вычислСний ΠΏΡ€ΠΈ ΠΊΠ°ΠΆΠ΄ΠΎΠΌ Ρ€Π΅Π½Π΄Π΅Ρ€Π΅.

НуТно ΠΏΠΎΠΌΠ½ΠΈΡ‚ΡŒ, Ρ‡Ρ‚ΠΎ функция, пСрСданная useMemo, запускаСтся во врСмя Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π°. НС Π½ΡƒΠΆΠ½ΠΎΒ Π΄Π΅Π»Π°Ρ‚ΡŒ Ρ‚Π°ΠΌ Π½ΠΈΡ‡Π΅Π³ΠΎ, Ρ‡Ρ‚ΠΎ ΠΎΠ±Ρ‹Ρ‡Π½ΠΎ нС дСлаСтся во врСмя Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π°. НапримСр, всС ΠΏΠΎΠ±ΠΎΡ‡Π½Ρ‹Π΅ эффСкты Π΄ΠΎΠ»ΠΆΠ΅Π½ Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡ‚ΡŒΒ useEffect, Π°Β Π½Π΅Β useMemo.

example:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Π”Π°Π»Π΅Π΅ структура ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° Π±Ρ‹Π»Π° Π½Π΅ΠΌΠ½ΠΎΠ³ΠΎ Ρ€Π΅ΠΎΡ€Π³Π°Π½ΠΈΠ·ΠΎΠ²Π°Π½Π° ΠΈ Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ Π³Π»Π°Π²Π½Ρ‹ΠΌ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠΌ Π±ΡƒΠ΄Π΅Ρ‚ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ страницы Posts.tsx

Π’ самом ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅ Posts.tsx ΠΌΡ‹ создадим ΠΎΠ±Ρ‰Π΅Π΅ состояниС ΠΏΠΎΠ΄ поиск, Π³Π΄Π΅ filter Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‚Π²Π΅Ρ‡Π°Ρ‚ΡŒ Π·Π° строку запроса query ΠΈ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ сСлСкта sort.

Π”Π°Π»Π΅Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ Π΄Π²Π΅ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ sortPosts ΠΈ sortedAndSearchedPosts, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ΡƒΡŽΡ‚ массив (ΠΏΠ΅Ρ€Π²Ρ‹ΠΉ ΠΏΠΎ сСлСкту, Π²Ρ‚ΠΎΡ€ΠΎΠΉ ΠΏΠΎ запросу) ΠΈ ΠΊΠ΅ΡˆΠΈΡ€ΡƒΡŽΡ‚ эту сортировку. Ѐункция sortPosts ΡƒΠ΄Π°Π»Π΅Π½Π°, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ Π΅Ρ‘ Π·Π°ΠΌΠ΅Π½ΡΡŽΡ‚ Π΄Π²Π΅ Π²Ρ‹ΡˆΠ΅ΠΎΠΏΠΈΡΠ°Π½Π½Ρ‹Π΅ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ.

Π”Π°Π»Π΅Π΅ ΠΌΡ‹ Π²Ρ‹Π·Π²Π°Π΅ΠΌ Ρ‚Ρ€ΠΈ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°: Ρ„ΠΎΡ€ΠΌΡƒ, Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ ΠΈ Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΡŽ списка постов.

page-components / Posts.tsx

import React, { useMemo, useRef, useState } from 'react';
import styles from './Posts.module.scss';
import { PostFilter } from '@/components';
import { PostForm } from '@/components';
import { PostList } from '@/components';
import { IPost } from '@/page-components/Posts/Posts.interface';
import { IFilter } from '@/components/PostFilter/PostFilter.props';
 
export const Posts = () => {
   const [posts, setPosts] = useState<IPost[]>([
      { id: 'asd1', title: 'Javascript', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
      { id: 'adsgsa2', title: 'C#', body: 'Π₯Ρ€ΠΎΡˆΠΈΠΉ язык' },
      { id: 'fsdagha3', title: 'Python', body: 'ΠŸΠΎΡ‡Π΅ΠΌΡƒ Π±Ρ‹ ΠΈ Π½Π΅Ρ‚?' },
   ]);
 
   // состояниС сСлСкта ΠΈ строки поиска
   const [filter, setFilter] = useState<IFilter>({ query: '', sort: 'title' });
 
   // ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ отсортированный массив
   const sortedPosts = useMemo<IPost[]>(() => {
      return [...posts].sort((a, b) => a[filter.sort].localeCompare(b[filter.sort]));
   }, [filter.sort, posts]);
 
   // сортируСм массив ΠΏΠΎ строкС поиска
   const sortedAndSearchedPosts = useMemo<IPost[]>(() => {
      return sortedPosts.filter(post =>
         post.title.toLowerCase().includes(filter.query.toLowerCase()),
      );
   }, [filter.query, sortedPosts]);
 
   const createPost = (newPost: IPost): void => {
      setPosts([...posts, newPost]);
   };
 
   const removePost = (post: IPost): void => {
      setPosts(posts.filter(p => p.id !== post.id));
   };
 
   return (
      <div className={styles.wrapper}>
         <PostForm create={createPost} />
         <PostFilter filter={filter} setFilter={setFilter} />
         <PostList className={styles.list} posts={sortedAndSearchedPosts} remove={removePost} />
      </div>
   );
};

Π”Π°Π»Π΅Π΅ ΠΈΠ΄Ρ‘Ρ‚ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ PostList, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ Π² сСбя Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ для удалСния поста ΠΈ массив постов.

components / PostList / PostList.props.ts

import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
import { IPost } from '@/page-components/Posts/Posts.interface';
 
export interface PostListProps
   extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
   remove: (post: IPost) => void;
   posts: IPost[];
}

Π’ΡƒΡ‚ выводится список постов, Π² ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ ΠΈΠ· ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… пСрСдаётся своя функция для ΠΈΡ… удалСния

components / PostList / PostList.tsx

export const PostList = ({ remove, posts, className, ...props }: PostListProps) => {
   return (
      <div className={cn(styles.wrapper, className)} {...props}>
         {/* Π²Ρ‹Π²ΠΎΠ΄ΠΈΠΌ ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ ΠΎΡ‚Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ ΠΊΠΎΠ½Π΅Ρ‡Π½Ρ‹ΠΉ массив */}
         {posts.length ? (
            posts.map(p => <PostItem remove={remove} key={p.id} post={p} />)
         ) : (
            <h2 style={{ textAlign: 'center' }}>ΠŸΠΎΡΡ‚Ρ‹ Π½Π΅ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Ρ‹</h2>
         )}
      </div>
   );
};

Π”Π°Π»Π΅Π΅ ΠΈΠ΄Ρ‘Ρ‚ Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ постов, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π² сСбя ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ пропс Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π° ΠΏΠΎ интСрфСйсу IFilter ΠΈ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ setFilter, которая устанавливаСт Π½ΠΎΠ²Ρ‹ΠΉ Ρ„ΠΈΠ»ΡŒΡ‚Ρ€.

Π‘Π°ΠΌ интСрфСйс IFilter прСдставляСт ΠΈΠ· сСбя интСрфСйс стСйта Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π° ΠΈΠ· Π³Π»Π°Π²Π½ΠΎΠ³ΠΎ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°

components / PostFilter / PostFilter.props.ts

import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
import { IPost } from '@/page-components/Posts/Posts.interface';
 
export interface IPostFilterProps
   extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
   filter: IFilter;
   setFilter: (filter: IFilter) => void;
}
 
export interface IFilter {
   sort: 'title' | 'body';
   query: string;
}

Π”Π°Π½Π½Ρ‹ΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ выполняСт Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΡŽ ΠΏΠΎ ΠΈΠ½ΠΏΡƒΡ‚Ρƒ ΠΈ ΠΏΠΎ сСлСкту

components / PostFilter / PostFilter.tsx

export const PostFilter = ({ filter, setFilter }: IPostFilterProps) => {
   return (
      <div className={styles.wrapper}>
         <Input
            className={styles.search}
            placeholder={'Поиск...'}
            value={filter.query}
            onChange={e => setFilter({ ...filter, query: e.target.value })}
         />
 
         <Divider />
 
         <Select
            value={filter.sort}
            onChange={(selectedSort: 'title' | 'body') =>
               setFilter({ ...filter, sort: selectedSort })
            }
            defaultValue={'Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠ°'}
            options={[
               { value: 'title', name: 'По Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΡƒ' },
               { value: 'body', name: 'По описанию' },
            ]}
         />
      </div>
   );
};

Π‘Π΄Π΅Π»Π°Π΅ΠΌ поиск ΠΏΠΎ ΡˆΠΈΡ€ΠΈΠ½Π΅ экрана

components / PostFilter / PostFilter.module.css

.search {
   width: 100%;
}

Π˜Ρ‚ΠΎΠ³: Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ поиск ΠΏΠΎ строкС запроса

01:23:50 ➝ МодальноС ΠΎΠΊΠ½ΠΎ. ΠŸΠ΅Ρ€Π΅ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡ‹ΠΉ UI ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚

Π§Ρ‚ΠΎΠ±Ρ‹ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π΅Ρ‰Ρ‘ ΠΎΠ΄ΠΈΠ½ ΡΡ‚ΠΈΠ»ΡŒ ΠΈΠ»ΠΈ нСсколько стилСй, ΠΌΠΎΠΆΠ½ΠΎ Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ Ρ‚Π°ΠΊΠΎΠΉ конструкциСй: [массив_классов].join(' '), Π³Π΄Π΅ ΠΌΡ‹ объСдиняСм вСсь массив Ρ‡Π΅Ρ€Π΅Π· Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ join()

Если Π½Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ класс ΠΏΠΎ ΡƒΡΠ»ΠΎΠ²ΠΈΡŽ, Ρ‚ΠΎ ΠΌΠΎΠΆΠ½ΠΎ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Ρ‚Π°ΠΊΡƒΡŽ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΡƒ:

ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ модального ΠΎΠΊΠ½Π° ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ Π² сСбя Π΄ΠΎΡ‡Π΅Ρ€Π½ΠΈΠ΅ элСмСнты, состояниС видимости ΠΈ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ установки этой видимости

components / Modal / Modal.props.ts

import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
 
export interface IModalProps
   extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
   children: ReactNode;
   visible: boolean;
   setVisible: (visible: boolean) => void;
}

Π’ΡƒΡ‚ ΡƒΠΆΠ΅ прСдставлСн ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Modal, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΈ Ρ€Π΅Π°Π»ΠΈΠ·ΡƒΠ΅Ρ‚ ΠΏΠΎΠ΄ собой ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ модального ΠΎΠΊΠ½Π° ΠΏΠΎ ΡΠΎΡΡ‚ΠΎΡΠ½ΠΈΡŽ, ΠΏΠ΅Ρ€Π΅Π΄Π°Π½Π½ΠΎΠΌΡƒ ΠΎΡ‚ родитСля.

Π’Π°ΠΊ ΠΆΠ΅ Ρ‚ΡƒΡ‚ ΠΏΠΎΠΊΠ°Π·Π°Π½ΠΎ, ΠΊΠ°ΠΊ ΠΌΠΎΠΆΠ½ΠΎ Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠ΅ΠΉ cn() ΠΈΠ· внСшнСго модуля classnames, которая упростит взаимодСйствиС с навСшиваниСм классов Π½Π° элСмСнты.

Π’ΡƒΡ‚ Π½ΡƒΠΆΠ½ΠΎ ΡΠΊΠ°Π·Π°Ρ‚ΡŒ, Ρ‡Ρ‚ΠΎ функция e.stopPropagation() (которая Π²Π»ΠΎΠΆΠ΅Π½Π° Π² Π΄ΠΎΡ‡Π΅Ρ€Π½ΠΈΠΉ элСмСнт ΠΎΠ±Ρ‘Ρ€Ρ‚ΠΊΠΈ) ΠΏΡ€Π΅Π΄ΠΎΡ‚Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Ρ… Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΉ, срабатываниС ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… повСшСно Π½Π° Π΄Π°Π½Π½Ρ‹ΠΉ элСмСнт ΠΈΠ»ΠΈ Π΅Π³ΠΎ Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»Π΅ΠΉ. ΠšΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½ΠΎ Π² Π΄Π°Π½Π½ΠΎΠΌ случаС, ΠΎΠ½ Π½Π΅ Π΄Π°Ρ‘Ρ‚ Π·Π°ΠΊΡ€Ρ‹Ρ‚ΡŒΡΡ ΠΌΠΎΠ΄Π°Π»ΡŒΠ½ΠΎΠΌΡƒ ΠΎΠΊΠ½Ρƒ ΠΏΡ€ΠΈ ΠΊΠ»ΠΈΠΊΠ΅ Π½Π° Π½Π΅Π³ΠΎ (функция закрытия Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π½Π° Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»Π΅ ΠΈΠ·Π½Π°Ρ‡Π°Π»ΡŒΠ½ΠΎ, Π½ΠΎ ΠΈ Π½Π° Ρ€Π΅Π±Ρ‘Π½ΠΊΠ΅)

components / Modal / Modal.tsx

import React from 'react';
import styles from './Modal.module.scss';
import cn from 'classnames';
import { IModalProps } from '@/components/Modal/Modal.props';
 
export const Modal = ({ children, className, visible, setVisible, ...props }: IModalProps): JSX.Element => {
   return (
      <div
         className={cn(styles.modal, className, {
            [styles.active]: visible,
            [styles.disabled]: !visible,
         })}
         onClick={() => setVisible(false)}
      >
         <div
	         className={styles.modal__content}
	         onClick={e => e.stopPropagation()}
	    >
            {children}
         </div>
      </div>
   );
};
  • Π’ Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΈΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Π½ΡƒΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ состояниС, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ Π±ΡƒΠ΄Π΅Ρ‚ ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π²ΠΈΠ΄ΠΈΠΌΠΎΡΡ‚ΡŒ модального ΠΎΠΊΠ½Π°.
  • Π”Π°Π»Π΅Π΅ Π² Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ createPost() Π΄ΠΎΠ±Π°Π²ΠΈΠΌ Π·Π°ΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ модального ΠΎΠΊΠ½Π° ΠΏΡ€ΠΈ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠΈ Π½ΠΎΠ²ΠΎΠ³ΠΎ поста
  • Π’ Ρ€Π΅Π½Π΄Π΅Ρ€Π΅ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΡƒΡŽ ΠΊΠ½ΠΎΠΏΠΊΡƒ, которая Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΡ€ΠΎΡΠ²Π»ΡΡ‚ΡŒ модальноС ΠΎΠΊΠ½ΠΎ
  • Π’Π½ΡƒΡ‚Ρ€ΡŒ ΠΌΠΎΠ΄Π°Π»ΠΊΠΈ ΠΏΠ΅Ρ€Π΅Π»ΠΎΠΆΠΈΠΌ Ρ„ΠΎΡ€ΠΌΡƒ для создания Π½ΠΎΠ²ΠΎΠ³ΠΎ поста

page-components / Posts.tsx

export const Posts = () => {
   const [posts, setPosts] = useState<IPost[]>([
      { id: 'asd1', title: 'Javascript', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
      { id: 'adsgsa2', title: 'C#', body: 'Π₯Ρ€ΠΎΡˆΠΈΠΉ язык' },
      { id: 'fsdagha3', title: 'Python', body: 'ΠŸΠΎΡ‡Π΅ΠΌΡƒ Π±Ρ‹ ΠΈ Π½Π΅Ρ‚?' },
   ]);
 
   const [filter, setFilter] = useState<IFilter>({ query: '', sort: 'title' });
 
   // состояниС модального ΠΎΠΊΠ½Π°
   const [modal, setModal] = useState(false);
 
   const sortedPosts = useMemo<IPost[]>(() => {
      return [...posts].sort((a, b) => a[filter.sort].localeCompare(b[filter.sort]));
   }, [filter.sort, posts]);
 
   const sortedAndSearchedPosts = useMemo<IPost[]>(() => {
      return sortedPosts.filter(post =>
         post.title.toLowerCase().includes(filter.query.toLowerCase()),
      );
   }, [filter.query, sortedPosts]);
 
   const createPost = (newPost: IPost): void => {
      setPosts([...posts, newPost]);
 
      // послС создания ΠΌΠΎΠ΄Π°Π»ΠΊΠΈ, ΠΎΠ½ΠΎ закроСтся
      setModal(false);
   };
 
   const removePost = (post: IPost): void => {
      setPosts(posts.filter(p => p.id !== post.id));
   };
 
   return (
      <div className={styles.wrapper}>
         <Button
	         className={styles.button}
	         buttonType={'purple'}
	         onClick={() => setModal(true)}
	     >
            Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ пост
         </Button>
 
         <Modal visible={modal} setVisible={setModal}>
            <PostForm create={createPost} />
         </Modal>
 
         <PostFilter filter={filter} setFilter={setFilter} />
 
         <PostList className={styles.list} posts={sortedAndSearchedPosts} remove={removePost} />
      </div>
   );
};

Π˜Ρ‚ΠΎΠ³:

01:30:23 ➝ Анимации. React transition group

Одним ΠΈΠ· способов Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π°Π½ΠΈΠΌΠ°Ρ†ΠΈΡŽ Π² Ρ€Π΅Π°ΠΊΡ‚ являСтся react-transition-group, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΊΠ°ΠΊ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ Ρ€Π΅Π°ΠΊΡ‚Π°

npm install react-transition-group --save &&
npm install --save @types/react-transition-group

Π’ΡƒΡ‚ Π½ΡƒΠΆΠ½ΠΎ ΠΎΠ±Π΅Ρ€Π½ΡƒΡ‚ΡŒ Π³Ρ€ΡƒΠΏΠΏΡƒ элСмСнтов Π² TransitionGroup Π° ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ элСмСнт Π² CSSTransition (Π² этот ΠΆΠ΅ элСмСнт Π½ΡƒΠΆΠ½ΠΎ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ свойство ΠΊΠ»ΡŽΡ‡Π°, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ ΠΎΠ½ располагаСтся Π²Ρ‹ΡˆΠ΅, Ρ‡Π΅ΠΌ PostItem)

components / PostList / PostList.tsx

import { TransitionGroup, CSSTransition } from 'react-transition-group';
 
export const PostList = ({ remove, posts, className, ...props }: PostListProps) => {
   return (
      <div className={cn(styles.wrapper, className)} {...props}>
         {posts.length ? (
            <TransitionGroup className='post-list'>
               {posts.map(p => (
                  <CSSTransition key={p.id} timeout={500} classNames='post'>
                     <PostItem remove={remove} post={p} />
                  </CSSTransition>
               ))}
            </TransitionGroup>
         ) : (
            <h2 style={{ textAlign: 'center' }}>ΠŸΠΎΡΡ‚Ρ‹ Π½Π΅ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Ρ‹</h2>
         )}
      </div>
   );
};

Π‘Ρ‚ΠΈΠ»ΠΈ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°: CSSTransition ΠΌΡ‹ Π½Π°ΠΈΠΌΠ΅Π½ΠΎΠ²Π°Π»ΠΈ ΠΊΠ°ΠΊ post ΠΈ поэтому Π² ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½Ρ‹Ρ… состояниях Π½ΡƒΠΆΠ½ΠΎ Π½Π°ΠΈΠΌΠ΅Π½ΠΎΠ²Π°Ρ‚ΡŒ Π½Π°Ρ‡Π°Π»ΡŒΠ½Ρ‹ΠΉ элСмСнт ΠΊΠ°ΠΊ post

.post-enter {
   opacity: 0;
}
.post-enter-active {
   opacity: 1;
   transition: all 0.2s ease-in;
}
.post-exit {
   opacity: 1;
}
.post-exit-active {
   opacity: 0;
   transform: rotateY(-350deg);
   transition: all 0.2s ease-in;
}

01:33:40 ➝ ДСкомпозиция. ΠšΠ°ΡΡ‚ΠΎΠΌΠ½Ρ‹Π΅ Ρ…ΡƒΠΊΠΈ

Когда наш ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Π½Π°Ρ‡ΠΈΠ½Π°Π΅Ρ‚ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒ большоС количСство Π»ΠΎΠ³ΠΈΠΊΠΈ, Π½ΡƒΠΆΠ½ΠΎ Π΅Π³ΠΎ Π΄Π΅ΠΊΠΎΠΌΠΏΠΎΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΈ Π²Ρ‹Π΄Π΅Π»ΡΡ‚ΡŒ Π΅Π³ΠΎ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ Π² Ρ…Π΅Π»ΠΏΠ΅Ρ€Ρ‹ ΠΈΠ»ΠΈ Π΄Π°ΠΆΠ΅ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ…ΡƒΠΊΠΈ.

Π’ΡƒΡ‚ Π±ΡƒΠ΄ΡƒΡ‚ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒΡΡ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ…ΡƒΠΊΠΈ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ Π²Ρ‹Ρ†Π΅ΠΏΠΈΠ»ΠΈ ΠΈΠ· основного ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° создания постов.

hooks / usePosts.ts

import { useMemo } from 'react';
import { IPost } from '@/page-components/Posts/Posts.interface';
 
const useSortedPosts = (posts: IPost[], sort: 'title' | 'body') => {
   return useMemo<IPost[]>(() => {
      return [...posts].sort((a, b) => a[sort].localeCompare(b[sort]));
   }, [sort, posts]);
};
 
export const usePosts = (posts: IPost[], sort: 'title' | 'body', query: string) => {
   const sortedPosts = useSortedPosts(posts, sort);
 
   return useMemo<IPost[]>(() => {
      return sortedPosts.filter(post => post.title.toLowerCase().includes(query.toLowerCase()));
   }, [query, sortedPosts]);
};

Π’ самом ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅ ΠΌΡ‹ просто Π²Ρ‹Π·Π²Π°Π΅ΠΌ сортировку постов

page-components / Posts.tsx

export const Posts = () => {
   const [posts, setPosts] = useState<IPost[]>([
      { id: 'asd1', title: 'Javascript', body: 'Π›ΡƒΡ‡ΡˆΠΈΠΉ язык Π½Π° Π—Π΅ΠΌΠ»Π΅' },
      { id: 'adsgsa2', title: 'C#', body: 'Π₯Ρ€ΠΎΡˆΠΈΠΉ язык' },
      { id: 'fsdagha3', title: 'Python', body: 'ΠŸΠΎΡ‡Π΅ΠΌΡƒ Π±Ρ‹ ΠΈ Π½Π΅Ρ‚?' },
   ]);
 
   const [filter, setFilter] = useState<IFilter>({ query: '', sort: 'title' });
 
   const [modal, setModal] = useState(false);
 
   // сортировка массива постов
   const sortedAndSearchedPosts = usePosts(posts, filter.sort, filter.query);
 
   const createPost = (newPost: IPost): void => {
      setPosts([...posts, newPost]);
 
      setModal(false);
   };
 
   const removePost = (post: IPost): void => {
      setPosts(posts.filter(p => p.id !== post.id));
   };
 
   return (
      <div className={styles.wrapper}>
         <Button className={styles.button} buttonType={'purple'} onClick={() => setModal(true)}>
            Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ пост
         </Button>
 
         <Modal visible={modal} setVisible={setModal}>
            <PostForm create={createPost} />
         </Modal>
 
         <PostFilter filter={filter} setFilter={setFilter} />
 
         <PostList className={styles.list} posts={sortedAndSearchedPosts} remove={removePost} />
      </div>
   );
};

01:36:20 ➝ Π Π°Π±ΠΎΡ‚Π° с сСрвСром. Axios

ΠŸΠ΅Ρ€Π²Ρ‹ΠΌ Π΄Π΅Π»ΠΎΠΌ, установим ΠΌΠΎΠ΄ΡƒΠ»ΡŒ ΠΏΠΎ Ρ€Π°Π±ΠΎΡ‚Π΅ с сСрвСром axios, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΠΎΠ·Π²ΠΎΠ»ΠΈΡ‚ просто ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡ‚ΡŒ запросы Π½Π° ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Π΄Π°Π½Π½Ρ‹Ρ… Π½Π° сСрвСр

npm iΒ axios

01:38:40 ➝ Π–ΠΈΠ·Π½Π΅Π½Π½Ρ‹ΠΉ Ρ†ΠΈΠΊΠ» ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°. useEffect

Π–ΠΈΠ·Π½Π΅Π½Π½Ρ‹ΠΉ Ρ†ΠΈΠΊΠ» ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° React дСлится Π½Π° 4 части:

  • Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ (ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ пропсы ΠΈ состояния)
  • ΠœΠΎΠ½Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ (вСшаСм ΡΠ»ΡƒΡˆΠ°Ρ‚Π΅Π»ΠΈ события, Π³Π΅Π½Π΅Ρ€ΠΈΠΌ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚)
  • ОбновлСниС (ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΠΌ ΠΊΠ°ΠΊΠΈΠ΅-Π»ΠΈΠ±ΠΎ дСйствия Π½Π°Π΄ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠΌ)
  • Π Π°Π·ΠΌΠΎΠ½Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ (удаляСм ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ / отписываСмся ΠΎΡ‚ ΡΠ»ΡƒΡˆΠ°Ρ‚Π΅Π»Π΅ΠΉ события, ΠΎΡ‡ΠΈΡ‰Π°Π΅ΠΌ глобальноС Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π΅)

И Ρ‚ΡƒΡ‚ ΠΌΡ‹ ΠΏΠΎΠ΄Ρ…ΠΎΠ΄ΠΈΠΌ ΠΊ использованию Ρ…ΡƒΠΊΠ° useEffect, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΠΎΠ·Π²ΠΎΠ»ΠΈΡ‚ Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡ‚ΡŒ дСйствия ΠΏΠΎΠ΄ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½Ρ‹Π΅ стадии ΠΌΠΎΠ½Ρ‚ΠΈΡ€ΠΎΠ²ΠΊΠΈ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°

Π’ Π³Π»Π°Π²Π½ΠΎΠΌ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠΌ массив постов с сСрвСра Ρ‡Π΅Ρ€Π΅Π· fetchPosts ΠΈ сохраним Π΅Π³ΠΎ Π² стСйт Π½Π°ΡˆΠΈΡ… постов. Π’Ρ‹Π·Π²Π°Π½Π° эта функция Π±ΡƒΠ΄Π΅Ρ‚ Ρ‡Π΅Ρ€Π΅Π· useEffect, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π² прСдставлСнном сСтапС Π±ΡƒΠ΄Π΅Ρ‚ Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡ‚ΡŒ дСйствиС Ρ€ΠΎΠ²Π½ΠΎ ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π· - ΠΏΡ€ΠΈ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ΅ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°

page-components / Posts.tsx

export const Posts = () => {
   const [posts, setPosts] = useState('');
 
   const fetchPosts = async () => {
      const posts = await axios.get('https://jsonplaceholder.typicode.com/posts');
      setPosts(posts.data);
   };
 
   useEffect(() => {
      fetchPosts();
   }, []);
 
   const [filter, setFilter] = useState<IFilter>({ query: '', sort: 'title' });
 
   const [modal, setModal] = useState(false);
 
   const sortedAndSearchedPosts = usePosts(posts, filter.sort, filter.query);
 
   const createPost = (newPost: IPost): void => {
      setPosts([...posts, newPost]);
 
      setModal(false);
   };
 
   const removePost = (post: IPost): void => {
      setPosts(posts.filter(p => p.id !== post.id));
   };
 
   return (
      <div className={styles.wrapper}>
         <Button className={styles.button} buttonType={'purple'} onClick={() => setModal(true)}>
            Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ пост
         </Button>
 
         <Modal visible={modal} setVisible={setModal}>
            <PostForm create={createPost} />
         </Modal>
 
         <PostFilter filter={filter} setFilter={setFilter} />
 
         <PostList className={styles.list} posts={sortedAndSearchedPosts} remove={removePost} />
      </div>
   );
};

Π˜Ρ‚ΠΎΠ³: ΠΌΡ‹ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠ»ΠΈ посты с сСрвСра ΠΈ сразу ΠΈΡ… ΠΎΡ‚Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ»ΠΈ, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ Ρ‡Π΅Ρ€Π΅Π· useEffect Π²Ρ‹Π·Π²Π°Π»ΠΈ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ получСния постов

01:43:08 ➝ API. PostService

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΠΏΡ€ΠΎΡΡ‚ΠΈΡ‚ΡŒ свою Ρ€Π°Π±ΠΎΡ‚Ρƒ с сСрвСром Ρ‡Π΅Ρ€Π΅Π· внСсСниС Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠΉ абстракции, ΠΌΠΎΠΆΠ½ΠΎ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ΅ API Π½Π° Ρ„Ρ€ΠΎΠ½Ρ‚Π΅, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ наши посты с сСрвСра.

Π‘ΠΎΠ·Π΄Π°Π΄ΠΈΠΌ класс со статичной Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠ΅ΠΉ ΠΈ Π±ΡƒΠ΄Π΅ΠΌ ΡƒΠΆΠ΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ эту ΠΊΠΎΠ½ΡΡ‚Ρ€ΡƒΠΊΡ†ΠΈΡŽ для Ρ€Π°Π±ΠΎΡ‚Ρ‹ с сСрвСром

Π—Π°Ρ€Π°Π½Π΅Π΅ стоит ΡΠΊΠ°Π·Π°Ρ‚ΡŒ, Ρ‡Ρ‚ΠΎ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Ρ‚ΡŒ ΠΎΡˆΠΈΠ±ΠΊΡƒ Ρ‚ΡƒΡ‚ - ΠΏΠ»ΠΎΡ…ΠΎΠΉ ΠΏΠΎΠ΄Ρ…ΠΎΠ΄, поэтому ΠΏΠ΅Ρ€Π΅Π»ΠΎΠ²ΠΈΠΌ ΠΎΡˆΠΈΠ±ΠΊΡƒ Π² Π΄Ρ€ΡƒΠ³ΠΎΠΌ мСстС ΠΊΠΎΠ΄Π° β†’

API / post.service.ts

import axios from 'axios';
 
export default class PostService {
   static async getAll() {
      const posts = await axios.get('https://jsonplaceholder.typicode.com/posts');
      return posts.data;
   }
}

И Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ посты Π±ΠΎΠ»Π΅Π΅ Π»Π°ΠΊΠΎΠ½ΠΈΡ‡Π½Ρ‹ΠΌ ΠΈ понятным способом

page-components / Posts.tsx

const fetchPosts = async () => {
	const posts = await PostService.getAll();
	setPosts(posts);
};

01:44:45 ➝ Π˜Π½Π΄ΠΈΠΊΠ°Ρ†ΠΈΡ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Π΄Π°Π½Π½Ρ‹Ρ… с сСрвСра

ΠœΡ‹ создали состояниС isPostLoading, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‚Π²Π΅Ρ‡Π°Ρ‚ΡŒ Π·Π° состояниС Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ постов. Π’ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ fetchPosts Π΄ΠΎΠ±Π°Π²ΠΈΠΌ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ этого состояния (Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ Ρ‡Π΅Ρ€Π΅Π· Π½Π΅Π³ΠΎ ΠΈΠ΄Ρ‘Ρ‚ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Π΄Π°Π½Π½Ρ‹Ρ… с сСрвСра). И Π΄Π°Π»Π΅Π΅ Π² render ΡƒΠΊΠ°ΠΆΠ΅ΠΌ, Ρ‡Ρ‚ΠΎ ΠΎΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ°Ρ‚ΡŒ сообщСниС ΠΎ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ΅ Π½ΡƒΠΆΠ½ΠΎ ΠΏΠΎΠΊΠ° Π΅Π³ΠΎ состояниС Π½Π΅ ΠΏΠ΅Ρ€Π΅ΠΉΠ΄Ρ‘Ρ‚ Π² false

page-components / Posts.tsx

export const Posts = () => {
   const [posts, setPosts] = useState('');
   // состояниС для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ постов
   const [isPostLoading, setIsPostLoading] = useState(false);
 
   const fetchPosts = async () => {
      // сСйчас посты Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π½Π°Ρ‡Π½ΡƒΡ‚ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΡƒ, поэтому Π½ΡƒΠΆΠ½ΠΎ ΠΏΠΎΠΊΠ°Π·Π°Ρ‚ΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΡƒ
      setIsPostLoading(true);
      const posts = await PostService.getAll();
      setPosts(posts);
 
      // здСсь ΡƒΠΆΠ΅ посты Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½Ρ‹
      setIsPostLoading(false);
   };
 
   useEffect(() => {
      fetchPosts();
   }, []);
 
   /// CODE ...
 
   return (
      <div className={styles.wrapper}>
         <Button className={styles.button} buttonType={'purple'} onClick={() => setModal(true)}>
            Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ пост
         </Button>
 
         <Modal visible={modal} setVisible={setModal}>
            <PostForm create={createPost} />
         </Modal>
 
         <PostFilter filter={filter} setFilter={setFilter} />
 
         {isPostLoading ? (
            <h1>Π˜Π΄Ρ‘Ρ‚ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ°...</h1>
         ) : (
            <PostList
               className={styles.list}
               posts={sortedAndSearchedPosts}
               remove={removePost}
            />
	    )}
      </div>
   );
};

Π˜Ρ‚ΠΎΠ³:

01:46:20 ➝ ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Loader. Анимации

ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Π»ΠΎΠ°Π΄Π΅Ρ€Π° Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΡ€Π΅Π΄ΡΡ‚Π°Π²Π»ΡΡ‚ΡŒ ΠΈΠ· сСбя ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹ΠΉ <div>

components / Loader / Loader.tsx

export const Loader = ({ children, className }: ILoaderProps) => {
	return <div className={cn(styles.loader, className)}>{children}</div>;
};

А анимация Π±ΡƒΠ΄Π΅Ρ‚ бСсконСчной ΠΈ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½Π½ΠΎΠΉ Ρ‡Π΅Ρ€Π΅Π· ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹ΠΉ CSS

components / Loader / Loader.module.SCSS

.loader {
   display: flex;
   align-items: center;
   justify-items: center;
 
   text-align: center;
 
   width: 100px;
   height: 100px;
 
   border-radius: 50%;
   border: 3px dashed var(--primary);
 
   animation: rotate 1s linear infinite;
   transition: all 0.2s;
}
 
@keyframes rotate {
   from {
      transform: rotate(0deg) scale(1);
   }
 
   to {      transform: rotate(360deg) scale(1.2);
   }
}

Π’Π΅ΠΏΠ΅Ρ€ΡŒ останСтся Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π²ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ ΠΊΡ€ΡƒΡ‚ΠΈΠ»ΠΊΡƒ Π½Π° страницу

page-components / Posts.tsx

{isPostLoading ? (
	<div className={styles.loadPosition}>
		<Loader>Π˜Π΄Ρ‘Ρ‚ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ°...</Loader>
	</div>
) : (
	<PostList
		className={styles.list}
		posts={sortedAndSearchedPosts}
		remove={removePost}
	/>
)}

Π˜Ρ‚ΠΎΠ³: ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π° ΠΊΡ€ΡƒΡ‚ΠΈΠ»ΠΊΠ°, которая ΠΎΠΏΠΎΠ²Π΅Ρ‰Π°Π΅Ρ‚ ΠΎ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ΅

01:49:25 ➝ ΠšΠ°ΡΡ‚ΠΎΠΌΠ½Ρ‹ΠΉ Ρ…ΡƒΠΊ useFetching(). ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок

Π”Π°Π»Π΅Π΅ Π½Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ Ρ…ΡƒΠΊ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ ΠΈ Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡ‚ΡŒ Π΅Ρ‘, Π° Ρ‚Π°ΠΊ ΠΆΠ΅ Π±ΡƒΠ΄Π΅Ρ‚ ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ состояниС спиннСра Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ ΠΈ ΠΏΠ΅Ρ€Π΅Ρ…Π²Π°Ρ‚Ρ‹Π²Π°Ρ‚ΡŒ ΠΎΡˆΠΈΠ±ΠΊΡƒ, Ссли таковая ΠΏΡ€ΠΈΠ΄Ρ‘Ρ‚ Π½Π° страницу

hooks / useFetching.ts

import { useState } from 'react';
 
export const useFetching = (callback: Function): [Function, boolean, string] => {
   const [isLoading, setIsLoading] = useState<boolean>(false);
   const [error, setError] = useState<string>('');
 
   const fetching = async (): Promise<void> => {
      try {
         setIsLoading(true);
         await callback();
      } catch (e: unknown) {
         setError(e.message as string);
      } finally {
         setIsLoading(false);
      }
   };
 
   return [fetching, isLoading, error];
};

И Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ Π² Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΎΠΌ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Ρ…ΡƒΠΊ useFetching, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΠΎΠ·Π²ΠΎΠ»ΠΈΡ‚ Π½Π°ΠΌ ΡƒΠ±Ρ€Π°Ρ‚ΡŒ отслСТиваниС состояния Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Π²Π½ΡƒΡ‚Ρ€ΠΈ Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΎΠ³ΠΎ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°

Π’Π°ΠΊ ΠΆΠ΅ сдСлаСм Π²Ρ‹Π²ΠΎΠ΄ ошибки, Ссли таковая Π±ΡƒΠ΄Π΅Ρ‚ ΠΈΠΌΠ΅Ρ‚ΡŒΡΡ Ρ‡Π΅Ρ€Π΅Π· postsError

page-components / Posts.tsx

export const Posts = () => {
   const [posts, setPosts] = useState('');
 
	// Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Ρ…ΡƒΠΊ для
   const [fetchPosts, isPostLoading, postsError] = useFetching(async () => {
      const posts = await PostService.getAll();
      setPosts(posts);
   });
 
   useEffect(() => {
      fetchPosts();
   }, []);
 
   const [filter, setFilter] = useState<IFilter>({ query: '', sort: 'title' });
 
   const [modal, setModal] = useState(false);
 
   const sortedAndSearchedPosts = usePosts(posts, filter.sort, filter.query);
 
   const createPost = (newPost: IPost): void => {
      setPosts([...posts, newPost]);
 
      setModal(false);
   };
 
   const removePost = (post: IPost): void => {
      setPosts(posts.filter(p => p.id !== post.id));
   };
 
   return (
      <div className={styles.wrapper}>
         <Button className={styles.button} buttonType={'purple'} onClick={() => setModal(true)}>
            Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ пост
         </Button>
 
         <Modal visible={modal} setVisible={setModal}>
            <PostForm create={createPost} />
         </Modal>
 
         <PostFilter filter={filter} setFilter={setFilter} />
 
		{postsError && <h1>ΠŸΡ€ΠΎΠΈΠ·ΠΎΡˆΠ»Π° ошибка {postsError}</h1>}
 
         {isPostLoading ? (
            <div className={styles.loadPosition}>
               <Loader>Π˜Π΄Ρ‘Ρ‚ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ°...</Loader>
            </div>
         ) : (
            <PostList
               className={styles.list}
               posts={sortedAndSearchedPosts}
               remove={removePost}
            />         )}
      </div>
   );
};

Если ΠΌΡ‹ Π½Π΅ смоТСм ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ посты с сСрвСра, Ρ‚ΠΎ ΡƒΠ²ΠΈΠ΄ΠΈΠΌ Π²ΠΎΡ‚ Ρ‚Π°ΠΊΡƒΡŽ ΠΎΡˆΠΈΠ±ΠΊΡƒ:

01:54:15➝ ΠŸΠΎΡΡ‚Ρ€Π°Π½ΠΈΡ‡Π½Ρ‹ΠΉ Π²Ρ‹Π²ΠΎΠ΄. ΠŸΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΡ (pagination)

Π’Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ΡŒ сразу Π½Π° ΠΎΠ΄Π½ΠΎΠΉ страницС 100 постов - это Π½Π΅ самая Π»ΡƒΡ‡ΡˆΠ°Ρ идСя. Если Π΄Π°Π½Π½Ρ‹Π΅ посты Π±ΡƒΠ΄ΡƒΡ‚ ΠΈΠΌΠ΅Ρ‚ΡŒ Π΅Ρ‰Ρ‘ ΠΈ Ρ„ΠΎΡ‚ΠΎΠ³Ρ€Π°Ρ„ΠΈΠΈ, Ρ‚ΠΎ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° страницы Π±ΡƒΠ΄Π΅Ρ‚ Π΄ΠΎΠ»Π³ΠΎΠΉ ΠΈ устройство ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Ρ‚Π°ΠΊ ΠΆΠ΅ Π±ΡƒΠ΄Π΅Ρ‚ сильно Π½Π°Π³Ρ€ΡƒΠΆΠ΅Π½ΠΎ.

БСйчас стоит Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ ΠΏΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΠ΅ΠΉ - постраничной Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΎΠΉ страниц.

Π”ΠΎΠΌΠ΅Π½ jsonplaceholder позволяСт ΠΏΠΎ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½Ρ‹ΠΌ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Π°ΠΌ Π²Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ΡŒ Π»ΠΈΠΌΠΈΡ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠ΅ количСство постов ΠΈ ΠΌΠ΅Π½ΡΡ‚ΡŒ страницу. Π’ Ρ…Π΅Π΄Π΅Ρ€Π΅ (x-total-count) Ρ‚Π°ΠΊ ΠΆΠ΅ указываСтся максимальноС количСство постов, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ ΠΌΠΎΠΆΠ΅Ρ‚ Π²Ρ‹Π΄Π°Ρ‚ΡŒ запрос

API / post.service.ts

import axios from 'axios';
 
export default class PostService {
   static async getAll(limit: number = 10, page: number = 1) {
      return await axios.get('https://jsonplaceholder.typicode.com/posts', {
         params: {
            _limit: limit,
            _page: page,
         },
      });
   }
}

utilities / pages.utilities.ts

// Ρ‚ΡƒΡ‚ ΠΌΡ‹ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ количСство страниц Π² зависимости ΠΎΡ‚ ΠΎΠ±Ρ‰Π΅Π³ΠΎ количСства постов ΠΈ ΠΈΡ… максимального количСства Π½Π° страницС
export const getPageCount = (totalCount: number, limit: number): number => {
   // Π²Π΅Ρ€Π½Ρ‘ΠΌ число страниц, ΠΎΠΊΡ€ΡƒΠ³Π»Ρ‘Π½Π½ΠΎΠ΅ Π² Π±ΠΎΠ»ΡŒΡˆΡƒΡŽ сторону
   return Math.ceil(totalCount / limit);
};
 
// Ρ‚ΡƒΡ‚ ΠΌΡ‹ создадим массив страниц, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π±ΡƒΠ΄ΡƒΡ‚ Π²Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ΡŒ посты
export const getPagesArray = (totalPages: number) => {
   let result: number[] = [];
   for (let i = 0; i < totalPages; i++) {
      result.push(i + 1);
   }
   return result;
};

Π£ΠΆΠ΅ Π² основном ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅ страницы ΠΌΡ‹ создаём Ρ‚Ρ€ΠΈ Π½ΠΎΠ²Ρ‹Ρ… состояния, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π±ΡƒΠ΄ΡƒΡ‚ ΠΎΡ‚Π²Π΅Ρ‡Π°Ρ‚ΡŒ Π·Π° ΠΎΠ±Ρ‰Π΅Π΅ количСство страниц, Π»ΠΈΠΌΠΈΡ‚ постов Π½Π° страницС ΠΈ Π·Π° саму страницу.

Π’ Ρ…ΡƒΠΊΠ΅ useFetching ΠΌΡ‹ Ρ‚Π°ΠΊ ΠΆΠ΅ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ ΠΎΡ‚ ΠΎΡ‚Π²Π΅Ρ‚Π° ΠΈ Π²Ρ‹Π·Ρ‹Π²Π°Π΅ΠΌ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ getPageCount, которая Π΄Π΅Π»ΠΈΡ‚ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π½Π° Π»ΠΈΠΌΠΈΡ‚ ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ количСство страниц, Π° ΡƒΠΆΠ΅ дальшС setTotalPages устанавливаСт это количСство Π² качСствС количСства страниц.

ΠŸΠΎΡ‚ΠΎΠΌ Π² pagesArray ΠΌΡ‹ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ массив страниц (1, 2, 3…).

И ΡƒΠΆΠ΅ Π² Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ changePage ΠΌΡ‹ осущСствляСм Ρ„Π΅Ρ‚Ρ‡ΠΈΠ½Π³ Π½ΠΎΠ²Ρ‹Ρ… постов Π½Π° страницС. Π­Ρ‚Ρƒ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ Π²Ρ‹Π·Ρ‹Π²Π°Π΅Ρ‚ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Button. Кнопки рСндСрятся Π² зависимости ΠΎΡ‚ количСства элСмСнтов Π² массивС pagesArray.

page-components / Posts.tsx

export const Posts = () => {
   const [posts, setPosts] = useState('');
   const [filter, setFilter] = useState<IFilter>({ query: '', sort: 'title' });
   const [modal, setModal] = useState(false);
 
   // состояниС, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ Ρ…Ρ€Π°Π½ΠΈΡ‚ ΠΎΠ±Ρ‰Π΅Π΅ колчиСство постов
   const [totalPages, setTotalPages] = useState<number>(0);
   // состояниС Π»ΠΈΠΌΠΈΡ‚Π° постов
   const [limit, setLimit] = useState<number>(10);
   // состояниС страницы постов
   const [page, setPage] = useState<number>(1);
 
   const [fetchPosts, isPostLoading, postsError] = useFetching(async () => {
      const response = await PostService.getAll(limit, page);
      setPosts(response.data);
 
      // ΠΎΠ±Ρ‰Π΅Π΅ количСство постов ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΈΠ· Ρ…Π΅Π΄Π΅Ρ€Π° запроса
      const totalCount = response.headers['x-total-count'];
 
      // ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΎΠ±Ρ‰Π΅Π΅ количСство страниц
      setTotalPages(getPageCount(totalCount, limit));
   });
 
   // ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ массив Π½ΠΎΠΌΠ΅Ρ€ΠΎΠ² страниц
   let pagesArray: number[] = getPagesArray(totalPages);
 
   // Ρ‚ΡƒΡ‚ ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΡƒΡΡ‚Π°Π½Π°Π²Π»ΠΈΠ²Π°Ρ‚ΡŒ Π² состояниС Π²Ρ‹Π±Ρ€Π°Π½Π½ΡƒΡŽ страницу ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ
   const changePage = (page: number) => {
      setPage(page);
      fetchPosts();
   };
 
   useEffect(() => {
      fetchPosts();
   }, []);
 
   /// CODE ...
 
   return (
      <div className={styles.wrapper}>
         <Button className={styles.button} buttonType={'purple'} onClick={() => setModal(true)}>
            Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ пост
         </Button>
 
         <Modal visible={modal} setVisible={setModal}>
            <PostForm create={createPost} />
         </Modal>
 
         <PostFilter filter={filter} setFilter={setFilter} />
 
         {postsError && <h1>ΠŸΡ€ΠΎΠΈΠ·ΠΎΡˆΠ»Π° ошибка {postsError}</h1>}
 
         {isPostLoading ? (
            <div className={styles.loadPosition}>
               <Loader>Π˜Π΄Ρ‘Ρ‚ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ°...</Loader>
            </div>
         ) : (
            <PostList
               className={styles.list}
               posts={sortedAndSearchedPosts}
               remove={removePost}
            />
        )}
 
 
		{/* Ρ‚ΡƒΡ‚ ΡƒΠΆΠ΅ ΠΌΡ‹ Π²Ρ‹Π²ΠΎΠ΄ΠΈΠΌ ΠΊΠ½ΠΎΠΏΠΊΠΈ со страницами */}
         <div className={styles.buttonBlock}>
            {pagesArray.map(p => (
               <Button
                  onClick={() => changePage(p)}
                  key={p}
                  className={cn(styles.buttonPage, {
                     [styles.buttonPage__current]: page === p,
                  })}
                  buttonType={'gray'}
               >
                  {p}
               </Button>
            ))}
         </div>
      </div>
   );
};

ΠœΡ‹ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π»ΠΈ ΠΏΠΎΠ΄Π³Ρ€ΡƒΠ·ΠΊΡƒ Π½ΠΎΠ²Ρ‹Ρ… постов Π½Π° страницС, Π½ΠΎ Ρ‚ΡƒΡ‚ ΠΌΡ‹ ΡΡ‚ΠΎΠ»ΠΊΠ½ΡƒΠ»ΠΈΡΡŒ с ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠΎΠΉ, Ρ‡Ρ‚ΠΎ ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄Π΅ Π½Π° Ρ€Π°Π·Π½Ρ‹Π΅ страницы, Ρƒ нас загруТаСтся ΠΏΡ€ΠΎΡˆΠ»Π°Ρ выбранная страница

02:06:20 ➝ ОбьяснСниС ΠΌΠ΅Ρ…Π°Π½ΠΈΠ·ΠΌΠ° измСнСния состояния

Π’ΡƒΡ‚ Π½ΡƒΠΆΠ½ΠΎ ΡΠΊΠ°Π·Π°Ρ‚ΡŒ, Ρ‡Ρ‚ΠΎ Ρ…ΡƒΠΊΠΈ - это асинхронный процСсс, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ происходит Π²Π½ΡƒΡ‚Ρ€ΠΈ Ρ€Π΅Π°ΠΊΡ‚Π° Π½Π΅ сразу. Π€ΡƒΠ½ΠΊΡ†ΠΈΠΈ ΠΏΠΎ Ρ‚ΠΈΠΏΡƒ сСттСров useState копятся ΠΈ Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡŽΡ‚ΡΡ Ρ€Π°Π·ΠΎΠΌ, ΠΎΡ‚Ρ‡Π΅Π³ΠΎ ΠΈ нашС ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ страниц Π²Ρ‹ΡˆΠ΅ ΠΈ Π½Π΅ Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ сразу. Π­Ρ‚ΠΎ сдСлано для Ρ‚ΠΎΠ³ΠΎ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΠ·Π±Π΅ΠΆΠ°Ρ‚ΡŒ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹Ρ… манипуляций с DOM-Π΄Π΅Ρ€Π΅Π²ΠΎΠΌ

Π‘Π°ΠΌΡ‹ΠΉ простой способ Ρ€Π΅ΡˆΠΈΡ‚ΡŒ Π΄Π°Π½Π½ΡƒΡŽ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡƒ - это Π·Π°ΠΊΠΈΠ½ΡƒΡ‚ΡŒ ΠΈΠ·ΠΌΠ΅Π½ΡΡŽΡ‰Π΅Π΅ΡΡ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ Π² useEffect ΠΈ ΠΈΠ·ΠΌΠ΅Π½ΡΡ‚ΡŒ страницу Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‡Π΅Ρ€Π΅Π· Π½Π΅Π³ΠΎ. И Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ ΠΌΠΎΠΆΠ½ΠΎ ΠΈΠ· Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ changePage ΡƒΠ±Ρ€Π°Ρ‚ΡŒ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ Ρ„Π΅Ρ‚Ρ‡ΠΈΠ½Π³Π° постов, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ это Π΄Π΅Π»Π°Π΅Ρ‚ useEffect.

page-components / Posts.tsx

const changePage = (page: number) => {
   setPage(page);
};
 
useEffect(() => {
   fetchPosts();
}, [page]);

И Ρ‚Π°ΠΊ ΠΆΠ΅ ΠΌΠΎΠΆΠ½ΠΎ Π΄Π΅ΠΊΠΎΠΌΠΏΠΎΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ постраничный Π²Ρ‹Π²ΠΎΠ΄ Π² ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚.

ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ ΠΏΠ°Π³ΠΈΠ½Π°Ρ†ΠΈΠΈ Π±ΡƒΠ΄Π΅Ρ‚ Π² сСбя ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Ρ‚ΡŒ ΠΎΠ±Ρ‰Π΅Π΅ количСство страниц, страницу, Π½Π° ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΉ ΠΌΡ‹ сСйчас находимся ΠΈ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ измСнСния страницы

components / Pagination / Pagination.props.ts

import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
 
export interface IPaginationProps
   extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
   totalPages: number;
   page: number;
   changePage: (page: number) => void;
}

Π’ сам ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Ρ‚Π°ΠΊ ΠΆΠ΅ пСрСнСсём Π²Ρ‹Π·ΠΎΠ² Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ получСния массива страниц

components / Pagination / Pagination.tsx

export const Pagination = ({ totalPages, page, changePage }: IPaginationProps) => {
   let pagesArray: number[] = getPagesArray(totalPages);
 
   return (
      <div className={styles.buttonBlock}>
         {pagesArray.map(p => (
            <Button
               onClick={() => changePage(p)}
               key={p}
               className={cn(styles.buttonPage, {
                  [styles.buttonPage__current]: page === p,
               })}
               buttonType={'gray'}
            >
			{p}
            </Button>
         ))}
      </div>
   );
};

Π ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΈΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Π½Π° Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚ выглядит ΠΏΠΎΠ΄ΠΎΠ±Π½Ρ‹ΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ:

page-components / Posts.tsx

export const Posts = () => {
   const [posts, setPosts] = useState('');
   const [filter, setFilter] = useState<IFilter>({ query: '', sort: 'title' });
   const [modal, setModal] = useState(false);
 
   // состояниС, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ Ρ…Ρ€Π°Π½ΠΈΡ‚ ΠΎΠ±Ρ‰Π΅Π΅ колчиСство постов
   const [totalPages, setTotalPages] = useState<number>(0);
   // состояниС Π»ΠΈΠΌΠΈΡ‚Π° постов
   const [limit, setLimit] = useState<number>(10);
   // состояниС страницы постов
   const [page, setPage] = useState<number>(1);
 
   const [fetchPosts, isPostLoading, postsError] = useFetching(async () => {
      const response = await PostService.getAll(limit, page);
      setPosts(response.data);
 
      // ΠΎΠ±Ρ‰Π΅Π΅ количСство постов ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΈΠ· Ρ…Π΅Π΄Π΅Ρ€Π° запроса
      const totalCount = response.headers['x-total-count'];
 
      // ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΎΠ±Ρ‰Π΅Π΅ количСство страниц
      setTotalPages(getPageCount(totalCount, limit));
   });
 
   // Ρ‚ΡƒΡ‚ ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΡƒΡΡ‚Π°Π½Π°Π²Π»ΠΈΠ²Π°Ρ‚ΡŒ Π² состояниС Π²Ρ‹Π±Ρ€Π°Π½Π½ΡƒΡŽ страницу ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ
   const changePage = (page: number) => {
      setPage(page);
   };
 
   useEffect(() => {
      fetchPosts();
   }, [page]);
 
   const sortedAndSearchedPosts = usePosts(posts, filter.sort, filter.query);
 
   const createPost = (newPost: IPost): void => {
      setPosts([...posts, newPost]);
 
      setModal(false);
   };
 
   const removePost = (post: IPost): void => {
      setPosts(posts.filter(p => p.id !== post.id));
   };
 
   return (
      <div className={styles.wrapper}>
         <Button className={styles.button} buttonType={'purple'} onClick={() => setModal(true)}>
            Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ пост
         </Button>
 
         <Modal visible={modal} setVisible={setModal}>
            <PostForm create={createPost} />
         </Modal>
 
         <PostFilter filter={filter} setFilter={setFilter} />
 
         {postsError && <h1>ΠŸΡ€ΠΎΠΈΠ·ΠΎΡˆΠ»Π° ошибка {postsError}</h1>}
 
         {isPostLoading ? (
            <div className={styles.loadPosition}>
               <Loader>Π˜Π΄Ρ‘Ρ‚ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ°...</Loader>
            </div>
         ) : (
            <PostList
               className={styles.list}
               posts={sortedAndSearchedPosts}
               remove={removePost}
            />         )}
 
         <Pagination totalPages={totalPages} page={page} changePage={changePage} />
      </div>
   );
};

02:12:00 ➝ React router. ΠŸΠΎΡΡ‚Ρ€Π°Π½ΠΈΡ‡Π½Π°Ρ навигация. BrowserRouter, Route, Switch, Redirect

УстанавливаСм Ρ€ΠΎΡƒΡ‚Π΅Ρ€ для Ρ€Π΅Π°ΠΊΡ‚Π°

npm iΒ react-router-dom

Π’Π΅ΠΏΠ΅Ρ€ΡŒ ΠΌΡ‹ Π½Π΅ΠΌΠ½ΠΎΠ³ΠΎ пСрСструктурируСм ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅:

  • Π‘ΠΎΠ·Π΄Π°Π΄ΠΈΠΌ ΠΏΠ°ΠΏΠΊΡƒ pages, которая Π±ΡƒΠ΄Π΅Ρ‚ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒ Π½Π΅ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹Π΅ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹, Π° Ρ†Π΅Π»Ρ‹Π΅ страницы, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π±ΡƒΠ΄ΡƒΡ‚ Ρ€Π°ΡΠΏΠΎΠ»Π°Π³Π°Ρ‚ΡŒΡΡ Π² ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ
  • ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ App, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ являСтся стартовой Ρ‚ΠΎΡ‡ΠΊΠΎΠΉ прилоТСния, Π±ΡƒΠ΄Π΅Ρ‚ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒ Π² сСбС BrowserRouter, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°Ρ‚ΡŒ всС Ρ€ΠΎΡƒΡ‚Ρ‹ Π² ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ
  • Π§Ρ‚ΠΎΠ±Ρ‹ ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚ΡŒ страницу Π² качСствС ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ Ρ€ΠΎΡƒΡ‚Π°, Π½ΡƒΠΆΠ½ΠΎ ΠΏΠΎΠΌΠ΅ΡΡ‚ΠΈΡ‚ΡŒ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ страницы Π² ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ Ρ€ΠΎΡƒΡ‚Π° Route. Π‘Π°ΠΌ Route Π²Π½ΡƒΡ‚Ρ€ΡŒ сСбя ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ для Ρ€ΠΎΡƒΡ‚ΠΈΠ½Π³Π° ΠΈ ΠΏΡƒΡ‚ΡŒ, ΠΏΠΎ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌΡƒ страница Π΄ΠΎΠ»ΠΆΠ½Π° ΠΎΡ‚Ρ€ΠΈΡΠΎΠ²Ρ‹Π²Π°Ρ‚ΡŒΡΡ

Π’Π΅ΠΏΠ΅Ρ€ΡŒ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΠΎΠ»Π½ΠΎΡ†Π΅Π½Π½Ρ‹ΠΉ Ρ€ΠΎΡƒΡ‚ΠΈΠ½Π³ ΠΈ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ ΠΌΠ΅ΠΆΠ΄Ρƒ страницами, Π½ΡƒΠΆΠ½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Link, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ ΠΏΡƒΡ‚ΡŒ to, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ, Π² свою ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ, опрСдСляСт, ΠΊΡƒΠ΄Π° Π½ΡƒΠΆΠ½ΠΎ ΠΏΠ΅Ρ€Π΅ΠΉΡ‚ΠΈ Π½Π° страницС.

Π£ΠΆΠ΅ прСдставлСнная рСализация ΠΏΠΎΠ·Π²ΠΎΠ»ΠΈΡ‚ Π½Π°ΠΌ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ΠΈΡ‚ΡŒ со страницы Π½Π° страницу Π±Π΅Π· ΠΏΠ΅Ρ€Π΅Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ страницы

Π”Π°Π»Π΅Π΅, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΌΡ‹ ΠΌΠΎΠ³Π»ΠΈ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Ρ‰Π°Ρ‚ΡŒΡΡ ΠΏΠΎ Ρ€ΠΎΡƒΡ‚Π°ΠΌ ΠΈ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Ρ‚ΡŒ Π½Π΅ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ страницы, Π½ΡƒΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Switch, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Redirect, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΡƒΠΆΠ΅ Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΠ΅Ρ€Π΅Π½Π°ΠΏΡ€Π°Π²Π»ΡΡ‚ΡŒ нас Π½Π° Π΄Ρ€ΡƒΠ³ΡƒΡŽ страницу, Ссли Ρ‚Π°, Π½Π° ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ ΠΌΡ‹ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ΠΈΠΌ, Π½Π΅ Π±ΡƒΠ΄Π΅Ρ‚ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΠΎΠ²Π°Ρ‚ΡŒ

И Ρ‚Π°ΠΊ выглядит страница ошибки ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄Π΅ Π½Π΅ Π½Π΅ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΡƒΡŽ страницу

pages > Error.jsx

import React from 'react';
 
const Error = () => {
    return (
        <div>
            <h1 style={{color: 'red'}}>
                Π’Ρ‹ ΠΏΠ΅Ρ€Π΅ΡˆΠ»ΠΈ Π½Π° Π½Π΅ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΡƒΡŽ страницу!
            </h1>
        </div>
    );
};
 
export default Error;

Π’Π°ΠΊ ΠΆΠ΅ ΠΌΠΎΠΆΠ½ΠΎ пСрСнСсти всю Π»ΠΎΠ³ΠΈΠΊΡƒ Ρ€ΠΎΡƒΡ‚ΠΈΠ½Π³Π° Π² ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚

02:22:00 ➝ ДинамичСская навигация. useHistory, useParams. Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² ΠΊ посту

И Π΄Π°Π»Π΅Π΅ Π½Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ Ρ€ΠΎΡƒΡ‚Ρ‹ ΠΏΠΎΠ΄ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ пост, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΌΠΎΠΆΠ½ΠΎ Π±Ρ‹Π»ΠΎ ΠΏΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ ΠΏΠΎ Π½Π΅ΠΌΡƒ Π΄Π΅Ρ‚Π°Π»ΡŒΠ½ΡƒΡŽ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ

Π’ΡƒΡ‚ Π½ΡƒΠΆΠ½ΠΎ ΡƒΠΆΠ΅ Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ Ρ…ΡƒΠΊΠΎΠΌ useHistory, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ прСдоставляСт Ρ€Π΅Π°ΠΊΡ‚-Ρ€ΠΎΡƒΡ‚Π΅Ρ€-Π΄ΠΎΠΌ. Π­Ρ‚ΠΎΡ‚ Ρ…ΡƒΠΊ позволяСт Π½Π°ΠΌ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ ΠΏΠΎ страницам Π±Π΅Π· ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° Link. ΠšΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½ΠΎ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠΌ push для Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ Ρ€ΠΎΡƒΡ‚ΠΎΠ² ΠΏΠΎ нашим постам. ΠŸΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡΡƒΡ‰Π΅ΡΡ‚Π²Π»ΡΡ‚ΡŒΡΡ Π±Π΅Π· ΠΏΠΎΠΌΠΎΡ‰ΠΈ ссылок - ΠΌΡ‹ Π½Π°ΠΆΠ°Π»ΠΈ Π½Π° ΠΊΠ½ΠΎΠΏΠΊΡƒ ΠΈ ΠΏΠ΅Ρ€Π΅ΡˆΠ»ΠΈ Π½Π° Π½ΡƒΠΆΠ½ΡƒΡŽ страницу

components / PostItem.jsx

const PostItem = (props) => {
    const router = useHistory()
 
    return (
        <div className="post">
            <div className="post__content">
                <strong>{props.post.id}. {props.post.title}</strong>
                <div>
                    {props.post.body}
                </div>
            </div>
            <div className="post__btns">
                <MyButton onClick={() => router.push(`/posts/${props.post.id}`)}>
                    ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ
                </MyButton>
                <MyButton onClick={() => props.remove(props.post)}>
                    Π£Π΄Π°Π»ΠΈΡ‚ΡŒ
                </MyButton>
            </div>
        </div>
    );
};

И сСйчас наш Ρ€ΠΎΡƒΡ‚ заносится Π² ΠΏΠΎΠΈΡΠΊΠΎΠ²ΡƒΡŽ строку, Π½ΠΎ Π½ΠΈΡ‡Π΅Π³ΠΎ Π½Π΅ происходит, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ Ρ€ΠΎΡƒΡ‚ поста Π½Π΅ Π±Ρ‹Π» создан

Π‘ΠΎΠ·Π΄Π°Π΄ΠΈΠΌ Π²Ρ€Π΅ΠΌΠ΅Π½Π½ΡƒΡŽ страницу ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ поста

И Π΄Π°Π»Π΅Π΅ Π² ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅ с Ρ€ΠΎΡƒΡ‚Π°ΠΌΠΈ Π½ΡƒΠΆΠ½ΠΎ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ Π½Π° Π΄ΠΈΠ½Π°ΠΌΠΈΡ‡Π΅ΡΠΊΡƒΡŽ страницу. Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΠΊΠ°Π·Π°Ρ‚ΡŒ, Ρ‡Ρ‚ΠΎ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ страницы динамичСский, Π½ΡƒΠΆΠ½ΠΎ Π² ссылкС ΡƒΠΊΠ°Π·Π°Ρ‚ΡŒ :id Π΄Π²ΠΎΠ΅Ρ‚ΠΎΡ‡ΠΈΠ΅.

Π’Π°ΠΊ ΠΆΠ΅ Ρƒ нас ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ Π΄Π²Π° Ρ€ΠΎΡƒΡ‚Π°, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π½Π°Ρ‡ΠΈΠ½Π°ΡŽΡ‚ΡΡ Π½Π° /posts. Π§Ρ‚ΠΎΠ±Ρ‹ ΠΈΠ·Π±Π΅ΠΆΠ°Ρ‚ΡŒ ΠΊΠΎΠ½Ρ„Π»ΠΈΠΊΡ‚ΠΎΠ², Π½ΡƒΠΆΠ½ΠΎ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚ exact

Π”Π°Π»Π΅Π΅ Π½Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ ΠΏΠΎΠ΄Π³Ρ€ΡƒΠΆΠ°Ρ‚ΡŒ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΏΠΎ посту (Π΅Π³ΠΎ имя, тСкст ΠΈ Ρ‚Π°ΠΊ Π΄Π°Π»Π΅Π΅)

ΠŸΠ΅Ρ€Π²Ρ‹ΠΌ Π΄Π΅Π»ΠΎΠΌ, Π½ΡƒΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π² API ΠΌΠ΅Ρ‚ΠΎΠ΄, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ ΠΎΠ΄ΠΈΠ½ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½Ρ‹ΠΉ пост с сСрвСра getById ΠΈ Π΅Π³ΠΎ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ getCommentsByPostId, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π±ΡƒΠ΄ΡƒΡ‚ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ id поста

src > API > PostService.js

import axios from "axios";
 
export default class PostService {
    static async getAll(limit = 10, page = 1) {
        const response = await axios.get('https://jsonplaceholder.typicode.com/posts', {
            params: {
                _limit: limit,
                _page: page
            }
        })
        return response;
    }
 
    static async getById(id) {
        const response = await axios.get('https://jsonplaceholder.typicode.com/posts/' + id)
        return response;
    }
 
    static async getCommentsByPostId(id) {
        const response = await axios.get(`https://jsonplaceholder.typicode.com/posts/${id}/comments`)
        return response;
    }
}

БСйчас Π²Ρ€Π΅ΠΌΠ΅Π½Π½ΡƒΡŽ страницу поста ΠΏΠ΅Ρ€Π΅Π΄Π΅Π»Π°Π΅ΠΌ ΠΏΠΎΠ΄ ΠΏΠΎΠ»Π½ΠΎΡ†Π΅Π½Π½ΡƒΡŽ страницу, которая Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΏΠΎ посту с сСрвСра

Π”Π°Π»Π΅Π΅ ΠΈΠ΄Ρ‘Ρ‚ Ρ…ΡƒΠΊ useParams, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Ρ‚ΠΎΠΆΠ΅ ΠΈΠ΄Ρ‘Ρ‚ ΠΈΠ· Ρ€Π΅Π°ΠΊΡ‚-Ρ€ΠΎΡƒΡ‚Π΅Ρ€-Π΄ΠΎΠΌ ΠΈ позволяСт ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ пропсы ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½ΠΎΠ³ΠΎ элСмСнта ΠΏΠΎ ссылкС (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄Π΅ Π½Π° страницу поста Π΄Π°Π½Π½Ρ‹ΠΉ Ρ…ΡƒΠΊ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ id поста, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ Π² ссылкС этот ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ динамичСский)

НиТС прСдставлСна рСализация ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠΉ страницы с постом ΠΈ Π΅Π³ΠΎ коммСнтариями

import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useFetching } from '../hooks/useFetching';
import PostService from '../API/PostService';
import Loader from '../components/UI/Loader/Loader';
 
const PostIdPage = () => {
	// Ρ…ΡƒΠΊ получСния пропсов
	const params = useParams();
	// это состояниС Π±ΡƒΠ΄Π΅Ρ‚ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒ Ρ‚ΠΎ, Ρ‡Ρ‚ΠΎ Π½Π°ΠΌ Π²Π΅Ρ€Π½Ρ‘Ρ‚ сСрвСр
	const [post, setPost] = useState({});
 
	// Ρ‚ΡƒΡ‚ ΡƒΠΆΠ΅ Π±ΡƒΠ΄ΡƒΡ‚ Ρ€Π°ΡΠΏΠΎΠ»Π°Π³Π°Ρ‚ΡŒΡΡ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ поста
	const [comments, setComments] = useState([]);
 
	// этот Ρ…ΡƒΠΊ Π²Π΅Ρ€Π½Ρ‘Ρ‚ Π½Π°ΠΌ ΠΎΠ΄ΠΈΠ½ пост ΠΏΠΎ ΠΏΠ΅Ρ€Π΅Π΄Π°Π½Π½ΠΎΠΌΡƒ id
	const [fetchPostById, isLoading, error] = useFetching(async (id) => {
		// ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ пост с сСрвСра
		const response = await PostService.getById(id);
		// устанавливаСм Π΄Π°Π½Π½Ρ‹Π΅ поста Π² состояниС поста
		setPost(response.data);
	});
 
	// Ρ‚ΡƒΡ‚ ΠΌΡ‹ Ρ„Π΅Ρ‚Ρ‡ΠΈΠΌ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ поста
	const [fetchComments, isComLoading, comError] = useFetching(async (id) => {
		// ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ пост с сСрвСра
		const response = await PostService.getCommentsByPostId(id);
		// устанавливаСм ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ Π² состояниС
		setComments(response.data);
	});
 
	// эффСкт Π±ΡƒΠ΄Π΅Ρ‚ Ρ„Π΅Ρ‚Ρ‡ΠΈΡ‚ΡŒ Π½ΡƒΠΆΠ½Ρ‹Π΅ Π½Π°ΠΌ Π΄Π°Π½Π½Ρ‹Π΅ ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π²ΠΎΠΉ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ΅ страницы
	useEffect(() => {
		// Ρ‚ΡƒΡ‚ ΠΎΠ½ Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅ ΠΏΠΎ посту
		fetchPostById(params.id);
		// Ρ‚ΡƒΡ‚ ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅ ΠΏΠΎ коммСнтариям ΠΊ посту
		fetchComments(params.id);
	}, []);
 
	return (
		<div>
			<h1>Π’Ρ‹ ΠΎΡ‚ΠΊΡ€Ρ‹Π»ΠΈ страницу поста c ID = {params.id}</h1>
 
			{/* Ρ‚Π°ΠΊ ΠΆΠ΅ Π½ΡƒΠΆΠ½ΠΎ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Π°Ρ‚ΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΡƒ поста */}
			{isLoading ? (
				<Loader />
			) : (
				<div>
					{post.id}. {post.title}
				</div>
			)}
			<h1>ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ</h1>
 
			{/* ΠΈ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΡƒ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠ΅Π² ΠΊ посту */}
			{isComLoading ? (
				<Loader />
			) : (
				<div>
					{comments.map((comm) => (
						<div key={comm.id} style={{ marginTop: 15 }}>
							<h5>{comm.email}</h5>
							<div>{comm.body}</div>
						</div>
					))}
				</div>
			)}
		</div>
	);
};
 
export default PostIdPage;

02:33:10 ➝ Π£Π»ΡƒΡ‡ΡˆΠ°Π΅ΠΌ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΡŽ. ΠŸΡ€ΠΈΠ²Π°Ρ‚Π½Ρ‹Π΅ ΠΈ ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹Π΅ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Ρ‹

ΠŸΠ΅Ρ€Π²Ρ‹ΠΌ Π΄Π΅Π»ΠΎΠΌ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡΠΎΠΊΡ€Π°Ρ‚ΠΈΡ‚ΡŒ количСство описанных Ρ€ΠΎΡƒΡ‚ΠΎΠ² Π² Ρ€Π΅Ρ‚Ρ‘Ρ€Π½Π΅ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° Ρ€ΠΎΡƒΡ‚ΠΈΠ½Π³Π°, ΠΌΠΎΠΆΠ½ΠΎ вынСсти ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ Route Π² ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ массив ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠ²

Π”Π°Π»Π΅Π΅ Π² ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅ с Ρ€ΠΎΡƒΡ‚ΠΈΠ½Π³ΠΎΠΌ просто вывСсти всС Ρ€ΠΎΡƒΡ‚Ρ‹ Ρ‡Π΅Ρ€Π΅Π· ΠΌΠ°ΠΏΡƒ

БСйчас Π½ΡƒΠΆΠ½ΠΎ ΠΎΠΏΠΈΡΠ°Ρ‚ΡŒ всС ΠΏΡ€ΠΈΠ²Π°Ρ‚Π½Ρ‹Π΅ ΠΈ ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹Π΅ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Ρ‹ Π½Π° страницС

src > router > index.js

import About from "../pages/About";
import Posts from "../pages/Posts";
import PostIdPage from "../pages/PostIdPage";
import Login from "../pages/Login";
 
 
export const privateRoutes = [
    {path: '/about', component: About, exact: true},
    {path: '/posts', component: Posts, exact: true},
    {path: '/posts/:id', component: PostIdPage, exact: true},
]
 
export const publicRoutes = [
    {path: '/login', component: Login, exact: true},
]

И Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π²Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹Π΅ ΠΈΠ»ΠΈ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΏΡ€ΠΈΠ²Π°Ρ‚Π½Ρ‹Π΅ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Ρ‹, ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΏΠΎ Ρ‚Π΅Ρ€Π½Π°Ρ€Π½ΠΎΠΌΡƒ ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€Ρƒ Π²Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ΡŒ Ρ€Π°Π·Π½Ρ‹Π΅ конструкции с Ρ€Π°Π·Π½Ρ‹ΠΌΠΈ Ρ€ΠΎΡƒΡ‚Π°ΠΌΠΈ

Если isAuth = false, Ρ‚ΠΎ Π±ΡƒΠ΄Π΅Ρ‚ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΡ‚ΡŒΡΡ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄ΠΈΠ½ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ - Login

02:38:00 ➝ useContext. Π“Π»ΠΎΠ±Π°Π»ΡŒΠ½Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅. Авторизация ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ

Π”Π°Π»Π΅Π΅ Ρƒ нас ΠΈΠ΄Ρ‘Ρ‚ Ρ…ΡƒΠΊ useContext, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ позволяСт ΠΈΠ· любой Ρ‚ΠΎΡ‡ΠΊΠΈ прилоТСния ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅.

ΠŸΠΎΡ‚Ρ€Π΅Π±Π½ΠΎΡΡ‚ΡŒ Π² Π½Ρ‘ΠΌ появилась, ΠΊΠΎΠ³Π΄Π° появились слоТности с ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡Π΅ΠΉ пропсов ΠΌΠ΅ΠΆΠ΄Ρƒ большой Π²Π»ΠΎΠΆΠ΅Π½Π½ΠΎΡΡ‚ΡŒΡŽ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ² Π² ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ

Π§Ρ‚ΠΎΠ±Ρ‹ Π½Π°Ρ‡Π°Ρ‚ΡŒ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ контСкст, Π½Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ для Π½Π°Ρ‡Π°Π»Π° ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ сам контСкст, ΠΊ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌΡƒ ΠΌΡ‹ Π±ΡƒΠ΅ΠΌ ΠΎΠ±Ρ€Π°Ρ‰Π°Ρ‚ΡŒΡΡ:

src > context > index.js

import {createContext} from 'react'
 
export const AuthContext = createContext(null);

Π”Π°Π»Π΅Π΅ Π² Π³Π»Π°Π²Π½ΠΎΠΌ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅ прилоТСния Π½ΡƒΠΆΠ½ΠΎ ΠΎΠ±Π΅Ρ€Π½ΡƒΡ‚ΡŒ всё ΠΏΠΎΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅Π΅ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π² контСкст AuthContext, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΈ Π±ΡƒΠ΄Π΅Ρ‚ Ρ€Π°ΡΠΏΡ€ΠΎΡΡ‚Ρ€Π°Π½ΡΡ‚ΡŒ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅ Π½Π° всС ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹. Π‘Π°ΠΌΠΈΠΌ распространСниСм занимаСтся Provider этого контСкста. Π’ Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚ value ΠΌΡ‹ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘ΠΌ значСния, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Ρ…ΠΎΡ‚ΠΈΠΌ Ρ€Π°ΡΠΏΡ€ΠΎΡΡ‚Ρ€Π°Π½ΡΡ‚ΡŒ.

И Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ ΠΏΡ€ΠΈ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠΈ страницы с ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΌ постом ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ Π²ΡΡ‚Ρ€Π΅Ρ‚ΠΈΡ‚ΡŒ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡƒ, Ρ‡Ρ‚ΠΎ ΠΌΡ‹ выбрасываСмся Π½Π° страницу с постами. Π­Ρ‚ΠΎ происходит ΠΏΠΎ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΉ ΠΏΡ€ΠΈΡ‡ΠΈΠ½Π΅:

  • ΠΈΠ·Π½Π°Ρ‡Π°Π»ΡŒΠ½ΠΎ аутСнтификация стоит ΠΊΠ°ΠΊ false
  • Ρƒ нас грузится страница Π»ΠΎΠ³ΠΈΠ½Π°
  • ΠΏΠΎΡ‚ΠΎΠΌ ΠΌΡ‹ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π΄Π°Π½Π½Ρ‹Π΅ с локального Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π°, Ρ‡Ρ‚ΠΎ ΠΌΡ‹ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Π½Ρ‹
  • Ρƒ нас загруТаСтся ΠΎΠ΄ΠΈΠ½ ΠΈΠ· доступных Ρ€ΠΎΡƒΡ‚ΠΎΠ² - посты

Π§Ρ‚ΠΎΠ±Ρ‹ ΠΏΠΎΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡƒ, Π½ΡƒΠΆΠ½ΠΎ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ состояниС isLoading, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ Π½Π΅ даст Π½Π°ΠΌ Π²Ρ‹Π±Ρ€Π°ΡΡ‹Π²Π°Ρ‚ΡŒΡΡ ΠΎΠ±Ρ€Π°Ρ‚Π½ΠΎ, Π° просто Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ°Ρ‚ΡŒ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ

src > App.jsx

function App() {
    // состояниС зарСгистрированности ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ
    const [isAuth, setIsAuth] = useState(false);
    // состояниС ΠΏΠΎΠ»Π½ΠΎΠΉ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ прилоТСния
    const [isLoading, setLoading] = useState(true);
 
    // эффСкт Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π· ΠΏΡ€ΠΈ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ΅ состояниС ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ - Π΅ΡΡ‚ΡŒ Π²Ρ…ΠΎΠ΄ ΠΈΠ»ΠΈ Π½Π΅Ρ‚ Π΅Π³ΠΎ
    useEffect(() => {
        if (localStorage.getItem('auth')) {
            setIsAuth(true)
        }
 
        // Ρ‚Π°ΠΊ ΠΆΠ΅ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° сСйчас ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ΡΡ
        setLoading(false);
    }, [])
 
    return (
        // Π΄Π°Π»Π΅Π΅ создаём Ρ‚ΡƒΡ‚ ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€ ΠΎΡ‚ контСкста ΠΈ ΠΎΠ±ΠΎΡ€Π°Ρ‡ΠΈΠ²Π°Π΅ΠΌ Π² Π½Π΅Π³ΠΎ всё ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅
        // Π² Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚ value ΡƒΠΊΠ°Π·Ρ‹Π²Π°Π΅ΠΌ, ΠΊΠ°ΠΊΠΈΠ΅ Π΄Π°Π½Π½Ρ‹Π΅ Π±ΡƒΠ΄Π΅Ρ‚ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒ Π² сСбС контСкст
        <AuthContext.Provider value={{
            isAuth,
            setIsAuth,
            isLoading
        }}>
            <BrowserRouter>
                <Navbar/>
                <AppRouter/>
            </BrowserRouter>
        </AuthContext.Provider>
    )
}
 
export default App;

И Π΄Π°Π»Π΅Π΅ Π² ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π΅ Ρ€ΠΎΡƒΡ‚ΠΈΠ½Π³Π° ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ спокойно ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ состояниС Π²Ρ…ΠΎΠ΄Π° ΠΈ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ. Если ΠΌΡ‹ находимся Π² Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ΅, Ρ‚ΠΎ ΠΌΠΎΠΆΠ΅ΠΌ просто Π²Π΅Ρ€Π½ΡƒΡ‚ΡŒ Π»ΠΎΠ°Π΄Π΅Ρ€, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΠΎΠΊΠ°ΠΆΠ΅Ρ‚, Ρ‡Ρ‚ΠΎ Π΄Π°Π½Π½Ρ‹Π΅ Π·Π°Π³Ρ€ΡƒΠΆΠ°ΡŽΡ‚ΡΡ

Π”Π°Π»Π΅Π΅ ΠΏΠΎ ΡΠΎΡΡ‚ΠΎΡΠ½ΠΈΡŽ Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ Π±ΡƒΠ΄ΡƒΡ‚ ΠΏΠΎΠ΄Π³Ρ€ΡƒΠΆΠ°Ρ‚ΡŒΡΡ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½Ρ‹Π΅ Ρ€ΠΎΡƒΡ‚Ρ‹

src > components > AppRouter.jsx

const AppRouter = () => {
    // ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΈΠ· глобального контСкста состояниС Π²Ρ…ΠΎΠ΄Π° ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΈ состояниС Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ
	const { isAuth, isLoading } = useContext(AuthContext);
 
    // ΠΈΠ΄Ρ‘Ρ‚ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ°, Ссли страница Π΄ΠΎ сих ΠΏΠΎΡ€ загруТаСтся
	if (isLoading) {
		return <Loader />;
	}
 
	return isAuth ? (
		<Switch>
			{privateRoutes.map((route) => (
				<Route component={route.component} path={route.path} exact={route.exact} key={route.path} />
			))}
			<Redirect to='/posts' />
		</Switch>
	) : (
		<Switch>
			{publicRoutes.map((route) => (
				<Route component={route.component} path={route.path} exact={route.exact} key={route.path} />
			))}
			<Redirect to='/login' />
		</Switch>
	);
};
 
export default AppRouter;

И Ρ‚Π°ΠΊ выглядит ΠΏΠΎΠ΄Π³Ρ€ΡƒΠ·ΠΊΠ° Π΄Π°Π½Π½Ρ‹Ρ… ΠΏΠΎ постам

На страницС Π»ΠΎΠ³ΠΈΠ½Π°, ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΡƒΡΡ‚Π°Π½Π°Π²Π»ΠΈΠ²Π°Ρ‚ΡŒ Π°ΠΉΡ‚Π΅ΠΌ auth Π² локальноС Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π΅, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΈ Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‚Π²Π΅Ρ‡Π°Ρ‚ΡŒ Π·Π° Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΡŽ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π² систСмС

src > page > Login.jsx

import React, {useContext} from 'react';
import MyInput from "../components/UI/input/MyInput";
import MyButton from "../components/UI/button/MyButton";
import {AuthContext} from "../context";
 
const Login = () => {
    // состояниС Π²Ρ…ΠΎΠ΄Π° Π² систСму ΠΌΡ‹ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΈΠ· глобального контСкста
    const {isAuth, setIsAuth} = useContext(AuthContext);
 
    // эта функция Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΡΡ‚ΡŒ, Ρ‡Ρ‚ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π·Π°Π»ΠΎΠ³ΠΈΠ½Π΅Π½
    const login = event => {
        // ΠΏΡ€Π΅Π΄ΠΎΡ‚Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ ΠΏΠ΅Ρ€Π΅Π·Π°Π³Ρ€ΡƒΠ·ΠΊΡƒ ΠΎΡ‚ Ρ„ΠΎΡ€ΠΌΡ‹
        event.preventDefault();
 
        // устанавливаСм состояниС
        setIsAuth(true);
 
        // Π² локальнС Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π΅ ΠΏΠΎΠΌΠ΅Ρ‰Π°Π΅ΠΌ true
        localStorage.setItem('auth', 'true')
    }
 
    return (
        <div>
            <h1>Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Π° для Π»ΠΎΠ³ΠΈΠ½Π°</h1>
            {/* Ρ„ΠΎΡ€ΠΌΠ° Π»ΠΎΠ³ΠΈΠ½Π° */}
            <form onSubmit={login}>
                <MyInput type="text" placeholder="Π’Π²Π΅Π΄ΠΈΡ‚Π΅ Π»ΠΎΠ³ΠΈΠ½"/>
                <MyInput type="password" placeholder="Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΏΠ°Ρ€ΠΎΠ»ΡŒ"/>
                <MyButton>Π’ΠΎΠΉΡ‚ΠΈ</MyButton>
            </form>
        </div>
    );
};
 
export default Login;

Π”Π°Π»Π΅Π΅ Π² Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΎΠ½Π½ΠΎΠΌ Π±Π°Ρ€Π΅ ΠΌΠΎΠΆΠ½ΠΎ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ Π²Ρ‹Ρ…ΠΎΠ΄Π° ΠΈΠ· прилоТСния, которая ΡƒΠ΄Π°Π»ΠΈΡ‚ΡŒ Π°ΠΉΡ‚Π΅ΠΌ Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΈΠ· локального Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π°

src > components > Navbar.jsx

import React, {useContext} from 'react';
import {Link} from "react-router-dom";
import MyButton from "../button/MyButton";
import {AuthContext} from "../../../context";
 
const Navbar = () => {
    const {isAuth, setIsAuth} = useContext(AuthContext);
 
    // ΠΏΡ€ΠΈ Π²Ρ‹Ρ…ΠΎΠ΄Π΅ с сайта аутСнтификация Ρ‚ΠΎΠΆΠ΅ слСтаСт
    const logout = () => {
        setIsAuth(false);
        localStorage.removeItem('auth')
    }
 
    return (
        <div className="navbar">
            <MyButton onClick={logout}>
                Π’Ρ‹ΠΉΡ‚ΠΈ
            </MyButton>
            <div className="navbar__links">
                <Link to="/about">О сайтС</Link>
                <Link to="/posts">ΠŸΠΎΡΡ‚Ρ‹</Link>
            </div>
        </div>
    );
};
 
export default Navbar;

Π˜Ρ‚ΠΎΠ³: ΠΌΡ‹ ΠΈΠΌΠ΅Π΅ΠΌ страницу Π²Ρ…ΠΎΠ΄Π°, с ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΉ ΠΌΡ‹ Π½Π΅ ΠΌΠΎΠΆΠ΅ΠΌ ΠΏΡ€ΠΎΠΉΡ‚ΠΈ Π½ΠΈ Π½Π° ΠΊΠ°ΠΊΠΈΠ΅ ΠΏΡ€ΠΈΠ²Π°Ρ‚Π½Ρ‹Π΅ Ρ€ΠΎΡƒΡ‚Ρ‹ / Π»ΠΎΠ³ΠΈΠ½ создаёт ΠΎΡ‚ΠΌΠ΅Ρ‚ΠΊΡƒ Π² локальном Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π΅ ΠΎ Π²Ρ…ΠΎΠ΄Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ

ΠΈ Ρ‚Π°ΠΊ ΠΆΠ΅ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ Π²Ρ‹ΠΉΡ‚ΠΈ ΠΈΠ· систСмы

02:47:10 ➝ БСсконСчная Π»Π΅Π½Ρ‚Π°. ДинамичСская пагинация. useObserver

БСйчас Π½ΡƒΠΆΠ½ΠΎ Π½Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ Ρ…ΡƒΠΊ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΠΎΠ·Π²ΠΎΠ»ΠΈΡ‚ Π½Π°ΠΌ Π½Π°Π±Π»ΡŽΠ΄Π°Ρ‚ΡŒ, дошли Π»ΠΈ ΠΌΡ‹ Π΄ΠΎ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½ΠΎΠ³ΠΎ Π±Π»ΠΎΠΊΠ° Π½Π° страницС, Ρ‡Ρ‚ΠΎΠ±Ρ‹ спокойно Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΡƒΡŽ ΠΏΠΎΡ€Ρ†ΠΈΡŽ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π°.

Бвойство isIntersecting Ρƒ observer ΠΎΡ‚Π²Π΅Ρ‡Π°Π΅Ρ‚ Π·Π° Ρ‚ΠΎ, Π² Π·ΠΎΠ½Π΅ видимости Π»ΠΈ наш элСмСнт

hooks / useObserver.ts

import { useEffect, useRef } from 'react';
 
export const useObserver = (ref, canLoad: boolean, isLoading: boolean, callback: Function) => {
   // Ρ‚ΡƒΡ‚ Π±ΡƒΠ΄Π΅Ρ‚ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒΡΡ сам ΠΎΠ±Π·Ρ‘Ρ€Π²Π΅Ρ€
   const observer = useRef();
 
   // Π²Ρ‚ΠΎΡ€ΠΎΠΉ эффСкт Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‚Π²Π΅Ρ‡Π°Ρ‚ΡŒ Π·Π° отслСТиваниС элСмСнта, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π±ΡƒΠ΄Π΅Ρ‚ Ρ‚Ρ€ΠΈΠ³Π³Π΅Ρ€ΠΈΡ‚ΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΡƒ постов
   useEffect(() => {
      // Ссли ΠΌΡ‹ загруТаСмся, Ρ‚ΠΎ Π½ΠΎΠ²Ρ‹ΠΉ ΠΎΠ±Π·Ρ‘Ρ€Π²Π΅Ρ€ ΡΠΎΠ·Π΄Π°Π²Π°Ρ‚ΡŒ сСйчас Π½Π΅ Π½ΡƒΠΆΠ½ΠΎ
      if (isLoading) return;
 
      // Ссли ΠΎΠ±Π·Ρ‘Ρ€Π²Π΅Ρ€ Π·Π° Ρ‡Π΅ΠΌ-Ρ‚ΠΎ ΡƒΠΆΠ΅ слСдит, Ρ‚ΠΎ Π½ΡƒΠΆΠ½ΠΎ ΡƒΠ±Ρ€Π°Ρ‚ΡŒ с Π½Π΅Π³ΠΎ всС слСТки Π½Π° Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ
      if (observer.current) observer.current?.disconnect();
 
      const callbackObserver = (entries, observer) => {
         // Ссли ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ Π² Π·ΠΎΠ½Π΅ видимости ΠΈ Ссли Π½ΠΎΠΌΠ΅Ρ€ Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΉ страницы мСньшС ΠΎΠ±Ρ‰Π΅Π³ΠΎ количСства страниц
         if (entries[0].isIntersecting && canLoad) {
            // Ρ‚ΠΎ измСняСм Π½ΠΎΠΌΠ΅Ρ€ страницы
            callback();
         }
      };
 
      // ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅ΠΌ Π½ΠΎΠ²Ρ‹ΠΉ ΠΎΠ±Π·Ρ‘Ρ€Π²Π΅Ρ€
      observer.current = new IntersectionObserver(callbackObserver);
      // Π²Ρ‹Π±ΠΈΡ€Π°Π΅ΠΌ отслСТиваСмый элСмСнт
      observer.current.observe(ref.current);
 
      // ΡΡ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Ρ‚ΡŒ эффСкт Π΄ΠΎΠ»ΠΆΠ΅Π½ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‚ΠΎΠ³Π΄Π°, ΠΊΠΎΠ³Π΄Π° измСнилось состояниС Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ страницы
   }, [isLoading]);
};

Π’ return добавляСм Π΄ΠΈΠ²-ΠΏΡƒΡΡ‚Ρ‹ΡˆΠΊΡƒ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π±ΡƒΠ΄Π΅Ρ‚ просто ΠΈΠΌΠ΅Ρ‚ΡŒ Π² сСбС рСфСрСнс lastElement, Π·Π° ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΌ ΠΈ Π±ΡƒΠ΄Π΅Ρ‚ ΡΠ»Π΅Π΄ΠΈΡ‚ΡŒ ΠΎΠ±Π·Ρ‘Ρ€Π²Π΅Ρ€. Π”Π°Π»Π΅Π΅ Π½Π°ΠΌ просто Π½ΡƒΠΆΠ½ΠΎ Π²Ρ‹Π·Π²Π°Ρ‚ΡŒ самописный Ρ…ΡƒΠΊ useObserver ΠΈ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ Π² Π½Π΅Π³ΠΎ Π½ΡƒΠΆΠ½Ρ‹Π΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹

page-components / Posts.tsx

export const Posts = () => {
   const [posts, setPosts] = useState('');
   const [filter, setFilter] = useState<IFilter>({ query: '', sort: 'title' });
   const [modal, setModal] = useState(false);
 
   const [totalPages, setTotalPages] = useState<number>(0);
   const [limit, setLimit] = useState<number>(10);
   const [page, setPage] = useState<number>(1);
 
   // Ρ‚ΡƒΡ‚ ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒ ссылку Π½Π° послСдний элСмСнт страницы, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΡ€ΠΈ достиТСнии Π΅Π³ΠΎ, Ρƒ нас ΠΏΠΎΠ΄Π³Ρ€ΡƒΠΆΠ°Π»ΠΈΡΡŒ Π½ΠΎΠ²Ρ‹Π΅ посты
   const lastElement = useRef<HTMLDivElement>();
 
   const [fetchPosts, isPostLoading, postsError] = useFetching(async () => {
      const response = await PostService.getAll(limit, page);
 
      // ΠΏΠΎΠ΄Π³Ρ€ΡƒΠΆΠ°Π΅Ρ‚ Π½Π΅ просто Π½ΠΎΠ²Ρ‹Π΅ посты, Π° добавляСт ΠΏΠΎΠ΄Π³Ρ€ΡƒΠΆΠ΅Π½Π½Ρ‹Π΅ Π² ΠΎΠ±Ρ‰ΠΈΠΉ массив постов
      setPosts([...posts, ...response.data]);
 
      const totalCount = response.headers['x-total-count'];
 
      setTotalPages(getPageCount(totalCount, limit));
   });
 
   const changePage = (page: number) => {
      setPage(page);
   };
 
   // Ρ‚ΡƒΡ‚ Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡΡ кастомным Ρ…ΡƒΠΊΠΎΠΌ для отслСТивания ΠΊΠΎΠ½Ρ†Π° страницы
   useObserver(lastElement, page < totalPages, isPostLoading, () => {
      setPage(page + 1);
   });
 
   useEffect(() => {
      fetchPosts();
   }, [page, limit]);
 
   const sortedAndSearchedPosts = usePosts(posts, filter.sort, filter.query);
 
   const createPost = (newPost: IPost): void => {
      setPosts([...posts, newPost]);
 
      setModal(false);
   };
 
   const removePost = (post: IPost): void => {
      setPosts(posts.filter(p => p.id !== post.id));
   };
 
   return (
      <div className={styles.wrapper}>
         <Button className={styles.button} buttonType={'purple'} onClick={() => setModal(true)}>
            Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ пост
         </Button>
 
         <Modal visible={modal} setVisible={setModal}>
            <PostForm create={createPost} />
         </Modal>
 
         <PostFilter filter={filter} setFilter={setFilter} />
 
         <Select
            defaultValue={'ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ элСмСнтов Π½Π° страницС'}
            options={[
               { value: 5, name: '5' },
               { value: 10, name: '10' },
               { value: 25, name: '25' },
               { value: -1, name: 'ВсС' },
            ]}
            value={limit}
            onChange={value => setLimit(value)}
         />
 
         {postsError && <h1>ΠŸΡ€ΠΎΠΈΠ·ΠΎΡˆΠ»Π° ошибка {postsError}</h1>}
 
         <PostList className={styles.list} posts={sortedAndSearchedPosts} remove={removePost} />
 
         {/* это Π½Π°Π±Π»ΡŽΠ΄Π°Π΅ΠΌΡ‹ΠΉ div */}
         <div ref={lastElement} style={{ height: 1 }} />
 
         {isPostLoading && (
            <div className={styles.loadPosition}>
               <Loader>Π˜Π΄Ρ‘Ρ‚ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ°...</Loader>
            </div>
         )}
 
         <Pagination totalPages={totalPages} page={page} changePage={changePage} />
      </div>
   );
};

ΠŸΡ€ΠΈ достиТСнии Π½Π΅Π²ΠΈΠ΄ΠΈΠΌΠΎΠ³ΠΎ div, Ρƒ нас срабатываСт функция закинутая Π² observer

Π’Π΅ΠΏΠ΅Ρ€ΡŒ, ΠΏΡ€ΠΈ достиТСнии Π½ΠΈΠ·Π° страницы, Ρƒ нас автоматичСски ΠΏΠΎΠ΄Π³Ρ€ΡƒΠΆΠ°ΡŽΡ‚ΡΡ Π½ΠΎΠ²Ρ‹Π΅ посты ΠΈ ΠΏΠ΅Ρ€Π΅Π»ΠΈΡΡ‚Ρ‹Π²Π°ΡŽΡ‚ΡΡ страницы