React Hooks
Что такое крючки
-
React
Использование функциональных компонентов всегда пропагандировалось, но иногда необходимо использоватьstate
Или некоторые другие функции, только компоненты класса могут использоваться, потому что компоненты функции не имеют ни экземпляра, ни функции жизненного цикла, только компоненты класса. -
Hooks
даReact 16.8
Добавлена функция, позволяющая писатьclass
использовать в случаеstate
и другиеReact
характеристика. - Если вы пишете функциональный компонент и понимаете, что вам нужно добавить некоторые
state
, предыдущей практикой было преобразование другого вclass
. Теперь вы можете использовать непосредственно в существующих функциональных компонентахHooks
. -
use
началоReact API
обеHooks
.
Какие проблемы решают хуки?
- Логику состояния трудно использовать повторно
- Повторное использование логики состояния между компонентами затруднено и может потребовать
render props
(свойства рендеринга) илиHOC
(компоненты более высокого порядка), но независимо от того, является ли это атрибутом рендеринга или компонентом более высокого порядка, он будет обертывать слой родительского контейнера (обычно элемент div) за пределами исходного компонента,приводит к иерархической избыточности.
- Повторное использование логики состояния между компонентами затруднено и может потребовать
- Имеет тенденцию быть сложным и трудным в обслуживании
- Смешивание ненужной логики в функциях жизненного цикла (например: в
componentDidMount
Регистрация событий и другой логики вcomponentWillUnmount
Разгружайте события посередине, чтобы было легко писать рассеянный и несфокусированный метод записи.Bug
). - Компоненты класса перегружены доступом и обработкой состояния, что затрудняет разделение компонентов на более мелкие компоненты.
- Смешивание ненужной логики в функциях жизненного цикла (например: в
- это указывает на проблему
- Когда родительский компонент передает функцию дочернему компоненту, он должен быть связан
this
- Когда родительский компонент передает функцию дочернему компоненту, он должен быть связан
Преимущество крючков
- Три проблемы, которые могут оптимизировать компоненты класса
- Возможность повторного использования логики состояния без изменения структуры компонента (пользовательские хуки)
- Возможность разделения взаимосвязанных частей компонента на более мелкие функции (например, настройка подписок или запрос данных).
- Разделение опасений по поводу побочных эффектов
- Побочные эффекты относятся к логике, которая не возникает во время преобразования данных в представление, например
Ajax
запрос, доступ к родномуDOM
Элементы, локальный постоянный кеш, события привязки/отвязки, добавление подписок, установка таймеров, логирование и т. д. В прошлом эти побочные эффекты были записаны в функции жизненного цикла компонента класса.
- Побочные эффекты относятся к логике, которая не возникает во время преобразования данных в представление, например
Общие крючки
useState
-
React
Предположим, когда мы звоним несколько разuseState
, убедитесь, что порядок их вызова одинаков при каждом рендеринге. - Добавьте некоторые внутренние элементы в компонент, вызвав его в функциональном компоненте.
state
,React
сохранит это состояние при повторном рендеринге -
useState
Единственным параметром является начальныйstate
-
useState
вернет массив: astate
, обновлениеstate
Функция - Во время первоначального рендеринга возвращаемое состояние
state
с первым переданным параметромinitialState
такое же значение. Мы можем вызвать обновление в обработчике событий или в другом месте.state
Функция. это что-то вродеclass
компонентthis.setState
, но новую не поставитstate
и старыйstate
Слияние, но замена напрямую.
инструкции
const [state, setState] = useState(initialState);
Например
import React, { useState } from 'react';
function Counter() {
const [counter, setCounter] = useState(0);
return (
<>
<p>{counter}</p>
<button onClick={() => setCounter(counter + 1)}>counter + 1</button>
</>
);
}
export default Counter;
Каждый рендер — это отдельное замыкание
- Каждый рендер имеет свои реквизиты и состояние.
- Каждый рендер имеет свой обработчик событий.
- При нажатии на состояние обновления функциональный компонент будет вызываться снова, поэтому каждый рендеринг будет независимым, и последующие операции не повлияют на полученное значение.
Например
function Counter() {
const [counter, setCounter] = useState(0);
function alertNumber() {
setTimeout(() => {
// 只能获取到点击按钮时的那个状态
alert(counter);
}, 3000);
}
return (
<>
<p>{counter}</p>
<button onClick={() => setCounter(counter + 1)}>counter + 1</button>
<button onClick={alertNumber}>alertCounter</button>
</>
);
}
функциональное обновление
если новыйstate
Нам нужно использовать предыдущийstate
вычисляется, то функция обратного вызова может быть передана в качестве параметраsetState
. Функция обратного вызова получит предыдущийstate
и возвращает обновленное значение.
Например
function Counter() {
const [counter, setCounter] = useState(0);
return (
<>
<p>{counter}</p>
<button onClick={() => setCounter(counter => counter + 10)}>
counter + 10
</button>
</>
);
}
ленивая инициализация
-
initialState
Параметры будут работать только при первоначальном рендеринге компонентов, а при последующем рендеринге они будут игнорироваться. - Если начальный
state
Если его нужно получить сложным вычислением, вы можете передать функцию, вычислить в функции и вернуть исходное значение.state
, эта функция вызывается только при начальном рендеринге
Например
function Counter4() {
console.log('Counter render');
// 这个函数只在初始渲染时执行一次,后续更新状态重新渲染组件时,该函数就不会再被调用
function getInitState() {
console.log('getInitState');
// 复杂的计算
return 100;
}
let [counter, setCounter] = useState(getInitState);
return (
<>
<p>{counter}</p>
<button onClick={() => setCounter(counter + 1)}>+1</button>
</>
);
}
useEffect
- Эффект (побочный эффект): относится к логике, которая не возникает в процессе преобразования данных в представление, например запросы ajax, доступ к собственным элементам dom, локальные постоянные кэши, события привязки/отвязки, добавление подписок, установка таймеров, ведение журнала и т. д. . . .
- Операции с побочными эффектами можно разделить на две категории: те, которые нужно очищать, и те, которые не нужно очищать.
- Изменение dom, отправка ajax-запросов и выполнение других операций с побочными эффектами внутри функциональных компонентов (в данном случае на этапе рендеринга React) не допускаются, поскольку это может привести к необъяснимым ошибкам и нарушению согласованности пользовательского интерфейса.
- useEffect — это эффект-хук, который добавляет возможность манипулировать побочными эффектами в функциональные компоненты. Он имеет то же назначение, что и componentDidMount, componentDidUpdate и componentWillUnmount в компоненте класса, но объединен в один API.
- useEffect принимает функцию, которая будет выполняться после рендеринга компонента на экран, у функции есть требования: либо возвращать функцию, очищающую побочный эффект, либо ничего не возвращать
- В отличие от componentDidMount или componentDidUpdate, эффекты, отправленные с помощью useEffect, не блокируют обновление экрана браузером, что делает ваше приложение более отзывчивым. В большинстве случаев эффекты не должны выполняться синхронно. В отдельных случаях (например, при измерении макетов) существует отдельный хук useLayoutEffect, который можно использовать с тем же API, что и useEffect .
инструкции
const App => () => {
useEffect(()=>{})
// 或者
useEffect(()=>{},[...])
return <></>
}
Измените заголовок, используя компонент класса
В этом классе нам нужно написать повторяющийся код в обеих функциях жизненного цикла, потому что во многих случаях мы хотим делать одно и то же, когда компонент загружается и обновляется. Мы хотим, чтобы он выполнялся после каждого рендеринга, но компоненты класса React не предоставляют такой метод. Даже если мы извлечем метод, нам все равно придется вызывать его в двух местах.
class Counter extends React.Component{
state = {number:0};
add = ()=>{
this.setState({number:this.state.number+1});
};
componentDidMount(){
this.changeTitle();
}
componentDidUpdate(){
this.changeTitle();
}
changeTitle = ()=>{
document.title = `你已经点击了${this.state.number}次`;
};
render(){
return (
<>
<p>{this.state.number}</p>
<button onClick={this.add}>+</button>
</>
)
}
}
Используйте компонент useEffect для изменения заголовка
function Counter(){
const [number,setNumber] = useState(0);
// useEffect里面的这个函数会在第一次渲染之后和更新完成后执行
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
document.title = `你点击了${number}次`;
});
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
</>
)
}
Что делает useEffect?Используя этот хук, вы можете сказать компоненту React, что ему нужно что-то сделать после рендеринга. React сохраняет переданную вами функцию (назовем ее «эффектом») и вызывает ее после выполнения обновления DOM. В этом случае мы устанавливаем свойство заголовка документа, но мы также можем выполнять выборку данных или вызывать другие императивные API.
Зачем вызывать useEffect внутри компонента?Размещение useEffect внутри компонента дает нам прямой доступ к переменной состояния count (или другим реквизитам) в эффекте. Нам не нужен специальный API для его чтения — он уже хранится в области видимости функции. Хуки используют механизм закрытия JavaScript вместо того, чтобы вводить определенные API React, когда JavaScript уже предоставляет решение.
Будет ли useEffect выполняться после каждого рендера?Да, по умолчанию он выполняется после первого рендера и после каждого обновления. (Мы поговорим о том, как управлять этим позже.) Вам, вероятно, будет более удобной идея о том, что эффекты возникают «после рендеринга», и вам не нужно беспокоиться о «монтировании» или «обновлении». React гарантирует, что при каждом запуске эффекта DOM обновляется.
явные побочные эффекты
- Побочные эффекты также могут быть указаны путем функции Возвращает функцию того, насколько понятны побочные эффекты, чтобы предотвратить утечки памяти, функцию очистки будет выполнена перед удалением компонентов. Если компонент визуализируется несколько раз, предыдущий эффект очищается перед выполнением следующего эффекта.
function Counter(){
let [number,setNumber] = useState(0);
let [text,setText] = useState('');
// 相当于componentDidMount 和 componentDidUpdate
useEffect(()=>{
console.log('开启一个新的定时器')
let timer = setInterval(()=>{
setNumber(number=>number+1);
},1000);
// useEffect 如果返回一个函数的话,该函数会在组件卸载和更新时调用
// useEffect 在执行副作用函数之前,会先调用上一次返回的函数
// 如果要清除副作用,要么返回一个清除副作用的函数
// return ()=>{
// console.log('destroy effect');
// clearInterval($timer);
// }
});
// },[]);//要么在这里传入一个空的依赖项数组,这样就不会去重复执行
return (
<>
<input value={text} onChange={(event)=>setText(event.target.value)}/>
<p>{number}</p>
<button>+</button>
</>
)
}
Эффект пропуска для оптимизации производительности
- Массив зависимостей управляет выполнением useEffect.
- Если какое-то конкретное значение не меняется между повторными рендерингами, вы можете указать React пропустить вызов эффекта, передав массив в качестве второго необязательного параметра для useEffect.
- Если вы хотите выполнить эффект, который запускается только один раз (только когда компонент смонтирован), вы можете передать пустой массив ([]) в качестве второго параметра. Это сообщает React, что ваш эффект не зависит ни от каких значений в свойствах или состоянии, поэтому его никогда не нужно повторять.
function Counter(){
let [number,setNumber] = useState(0);
let [text,setText] = useState('');
// 相当于componentDidMount 和 componentDidUpdate
useEffect(()=>{
console.log('useEffect');
let timer = setInterval(()=>{
setNumber(number=>number+1);
},1000);
},[text]);// 数组表示 effect 依赖的变量,只有当这个变量发生改变之后才会重新执行 efffect 函数
return (
<>
<input value={text} onChange={(e)=>setText(e.target.value)}/>
<p>{number}</p>
<button>+</button>
</>
)
}
Разделение проблем с использованием нескольких эффектов
- Одной из целей использования Hook является решение проблемы, заключающейся в том, что функции жизненного цикла в классе часто содержат несвязанную логику, но связанная логика разделена на несколько разных методов.
// class版
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
-
Мы можем видеть, как логика document.title разбивается на
componentDidMount
а такжеcomponentDidUpdate
, как логика подписки делится наcomponentDidMount
а такжеcomponentWillUnmount
середина. а такжеcomponentDidMount
Содержит код для двух разных функций одновременно. Это может сделать функции жизненного цикла очень запутанными. -
Хуки позволяют нам разделить код по назначению, а не по функциям жизненного цикла.
React
последуетeffect
Порядок объявлений вызывает каждый из компонентов по очередиeffect
.
// Hooks 版
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
useContext
const value = useContext(MyContext);
получитьcontext
объект (возвращаемое значение React.createContext ) и возвращаетcontext
текущее значение . токcontext
Значение определяется ближайшим компонентом к текущему компоненту в верхнем компоненте.<MyContext.Provider>
Ценностная опора определена.
Когда самый верхний компонент компонента является ближайшим<MyContext.Provider>
При обновлении отображаетсяHook
вызовет повторный рендеринг с последним переданнымMyContext provider
изcontext value
стоимость. даже если предки используютReact.memo
илиshouldComponentUpdate
, также используется в самом компонентеuseContext
повторно рендерить.
Не забывайте, что аргументом useContext должен быть сам объект контекста:
- правильный:
useContext(MyContext)
- ошибка:
useContext(MyContext.Consumer)
- ошибка:
useContext(MyContext.Provider)
намекать если вы в контакте
Hook
уже правильноcontext API
привычнее, должно быть понятно,useContext(MyContext)
эквивалентноclass
в компонентеstatic contextType = MyContext
или<MyContext.Consumer>
.useContext(MyContext)
просто чтобы вы могли читатьcontext
стоимость и подпискаcontext
Изменение. Вам все еще нужно использовать в верхнем дереве компонентов<MyContext.Provider>
для предоставления компонентов более низкого уровняcontext。
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.light}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
Пользовательские крючки
- Пользовательские хуки — это скорее условность, чем функция. Функция называется пользовательским хуком, если ее имя начинается с use и вызывает другие хуки.
- Иногда мы хотим повторно использовать некоторую логику состояния между компонентами, либо с реквизитами рендеринга, либо с компонентами более высокого порядка, либо с избыточностью.
- Пользовательские хуки позволяют добиться того же результата без добавления компонентов.
- Хук — это способ повторного использования логики состояния, он не использует повторно само состояние.
- На самом деле каждый вызов Hook имеет полностью независимое состояние.
function useNumber(){
let [number,setNumber] = useState(0);
useEffect(()=>{
setInterval(()=>{
setNumber(number=>number+1);
},1000);
},[]);
return [number,setNumber];
}
// 每个组件调用同一个 hook,只是复用 hook 的状态逻辑,并不会共用一个状态
function Counter1(){
let [number,setNumber] = useNumber();
return (
<div><button onClick={()=>{
setNumber(number+1)
}}>{number}</button></div>
)
}
function Counter2(){
let [number,setNumber] = useNumber();
return (
<div><button onClick={()=>{
setNumber(number+1)
}}>{number}</button></div>
)
}
использоватьMemo, использоватьCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
существуетa
а такжеb
Когда значение переменной постоянно,memoizedCallback
Ссылки без изменений. который:useCallback
Первый параметр функции будет кэшироваться, чтобы достичь цели оптимизации производительности рендеринга.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
существуетa
а такжеb
Когда значение переменной не меняется,memoizedValue
значение не меняется. который:useMemo
Первая входная функция функции не будет выполняться, чтобы сохранить количество вычислений.
оптимизация производительности
Object.is мелкое сравнение
- Крючок для внутреннего использования
Object.is
сравнить старое и новоеstate
равны. - а также
class
в компонентеsetState
Метод отличается.Если вы изменяете состояние, а переданное значение состояния не меняется, оно не будет повторно отображаться. - а также
class
в компонентеsetState
разные методы,useState
Объекты обновления не объединяются автоматически. Вы можете использовать функциональныеsetState
В сочетании с оператором распространения для достижения эффекта слияния обновленных объектов.
function Counter(){
const [counter,setCounter] = useState({name:'计数器',number:0});
console.log('render Counter')
// 如果你修改状态的时候,传的状态值没有变化,则不重新渲染
return (
<>
<p>{counter.name}:{counter.number}</p>
<button onClick={()=>setCounter({...counter,number:counter.number+1})}>+</button>
<button onClick={()=>setCounter(counter)}>++</button>
</>
)
}
Сокращение времени рендеринга
- По умолчанию, пока состояние родительского компонента изменяется (независимо от того, зависит ли дочерний компонент от состояния или нет), дочерний компонент также будет перерисовываться.
- Общая оптимизация:
- Компонент класса: можно использовать
pureComponent
; - Функциональные компоненты: использование
React.memo
, передавая функциональный компонент вmemo
После этого он вернет новый компонент, функцию нового компонента:Если полученное свойство не изменилось, функция не перерисовывается..
- Компонент класса: можно использовать
- Но как сделать так, чтобы свойства не изменились? используется здесь
useState
, каждое обновление независимо,const [number,setNumber] = useState(0)
То есть каждый раз генерируется новое значение (даже если значение не меняется), даже если используется React.memo, оно все равно будет перерисовываться.
const SubCounter = React.memo(({onClick,data}) =>{
console.log('SubCounter render');
return (
<button onClick={onClick}>{data.number}</button>
)
})
const ParentCounter = () => {
console.log('ParentCounter render');
const [name,setName]= useState('计数器');
const [number,setNumber] = useState(0);
const data ={number};
const addClick = ()=>{
setNumber(number+1);
};
return (
<>
<input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>
<SubCounter data={data} onClick={addClick}/>
</>
)
}
- Более глубокая оптимизация - используйте
useMemo
&useCallback
const SubCounter = React.memo(({onClick,data}) =>{
console.log('SubCounter render');
return (
<button onClick={onClick}>{data.number}</button>
)
})
const ParentCounter = () => {
console.log('ParentCounter render');
const [name,setName]= useState('计数器');
const [number, setNumber] = useState(0);
// 父组件更新时,这里的变量和函数每次都会重新创建,那么子组件接受到的属性每次都会认为是新的
// 所以子组件也会随之更新,这时候可以用到 useMemo
// 有没有后面的依赖项数组很重要,否则还是会重新渲染
// 如果后面的依赖项数组没有值的话,即使父组件的 number 值改变了,子组件也不会去更新
//const data = useMemo(()=>({number}),[]);
const data = useMemo(()=>({number}),[number]);
const addClick = useCallback(()=>{
setNumber(number+1);
},[number]);
return (
<>
<input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>
<SubCounter data={data} onClick={addClick}/>
</>
)
}
Общая проблема
useEffect не может получить асинхронный вызов в качестве функции обратного вызова
React
РегулированиеuseEffect
Принимающая функция либо возвращает функцию, устраняющую побочные эффекты, либо ничего не возвращает. а такжеasync
то, что возвращаетсяpromise
.
Как элегантно получать данные с помощью хуков
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
// 更优雅的方式
const fetchData = async () => {
const result = await axios(
'https://api.github.com/api/v3/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
Не слишком полагайтесь на useMemo
-
useMemo
Также есть накладные расходы.useMemo
будет "запоминать" некоторые значения, а позжеrender
Когда значение в зависимом массиве извлекается и сравнивается со значением, записанным в последний раз, функция обратного вызова будет выполняться повторно, если они не равны, в противном случае будет возвращено «запомненное» значение напрямую. Сам этот процесс будет потреблять определенное количество памяти и вычислительных ресурсов. Поэтому чрезмерное использованиеuseMemo
Может повлиять на работу программы. -
В использовании
useMemo
Перед этим следует рассмотреть три вопроса:-
Перейти к
useMemo
Функция дорого стоит?Некоторые вычисления являются дорогостоящими, и нам нужно «запоминать» их возвращаемое значение, чтобы избежать каждый разrender
пересчитать. Если вы выполняете недорогую операцию, вам не нужно запоминать возвращаемое значение. В противном случае используйтеuseMemo
Стоимость сама по себе может перевешивать стоимость пересчета этого значения. Поэтому для некоторых простых операций JS нам не нужно использоватьuseMemo
чтобы «запомнить» его возвращаемое значение. - Является ли возвращаемое значение исходным значением?Если вычисляемое значение является примитивным типом (строка, логическое значение, null, undefined, число, символ), то каждое сравнение равно, и нижестоящий компонент не будет перерисовываться; если вычисляемое значение представляет собой сложный тип (объект, array), даже если значение не изменится, но изменится адрес, что приведет к повторному рендерингу нижестоящих компонентов. Так что нам тоже нужно «запомнить» это значение.
-
написание обычаев
Hook
Когда возвращаемое значение должно поддерживать согласованность ссылок. Потому что вы не можете быть уверены, как снаружи будет использоваться возвращаемое значение. Если возвращаемое значение используется для другихHook
зависимости, и каждый разre-render
Когда ссылка несовместима (когда значения равны), может возникнуть ошибка. Поэтому, если значение, представленное в пользовательском хуке, является объектом, массивом, функцией и т. д., его следует использовать.useMemo
. чтобы гарантировать, что ссылка не изменится, когда значение будет тем же самым.
-
Перейти к
TypeScript
Что такое TypeScript
TypeScript
даJavaScript
Надмножество , которое в основном обеспечиваетсистема типова такжеправильноES6
служба поддержки.
Почему стоит выбрать TypeScript
-
TypeScript повышает удобочитаемость и ремонтопригодность кода
- Система типов на самом деле является лучшей документацией, большинство функций можно использовать, просто взглянув на определения типов.
- Большинство ошибок можно найти во время компиляции, что лучше, чем ошибки во время выполнения.
- Расширьте возможности редактора и IDE, включая завершение кода, подсказку интерфейса, переход к определению, реконструкцию и т. д.
-
TypeScript очень инклюзивен
- TypeScript — это надмножество JavaScript, файлы .js можно напрямую переименовывать в .ts.
- Автоматический вывод типа даже без явного определения типа
- Может определить практически любой тип от простого до сложного
- Создавайте файлы JavaScript, даже если TypeScript компилирует ошибки
- Совместим со сторонними библиотеками, даже если сторонняя библиотека не написана на TypeScript, вы можете написать отдельный файл типа для чтения TypeScript
-
TypeScript имеет активное сообщество
- В большинстве сторонних библиотек есть файлы определения типов для TypeScript.
- TypeScript включает в себя спецификацию ES6, а также поддерживает некоторые черновые спецификации ESNext.
Зная React Hooks и TypeScript, давайте взглянем на их комбинацию вместе! 😄
упражняться
Эта практика исходит из проекта библиотеки компонентов с открытым исходным кодом, который я разрабатываю.Azir DesignсерединаGridКомпонент компоновки сетки.
Цель
API
Row
Атрибуты | иллюстрировать | Типы | По умолчанию |
---|---|---|---|
className | имя класса | string | - |
style | Стиль компонента строки | object:CSSProperties | - |
align | вертикальное выравнивание | верх|середина|низ | top |
justify | горизонтальное расположение | начало|конец|центр|пространство-вокруг|пространство-между | start |
gutter | Шаг сетки, может быть записан как значение в пикселях, чтобы установить горизонтальный и вертикальный интервал, или использовать форму массива, чтобы установить [горизонтальный интервал, вертикальный интервал] | число|[число,число] | 0 |
Col
Атрибуты | иллюстрировать | Типы | По умолчанию |
---|---|---|---|
className | имя класса | string | - |
style | Стиль компонента Col | object:CSSProperties | - |
flex | свойства гибкого макета | строка|число | - |
offset | Количество интервалов в левой части сетки, в интервале не может быть сеток | number | 0 |
order | порядок сетки | number | 0 |
pull | Переместить сетку влево | number | 0 |
push | Переместить сетку вправо | number | 0 |
span | Количество заполнителей сетки, когда оно равно 0, эквивалентно отображению: нет | number | - |
xs | Адаптивная сетка | число|объект | - |
sm | Адаптивная сетка ≥576 пикселей, которая может быть количеством сеток или объектом с другими свойствами | число|объект | - |
md | Адаптивная сетка ≥768 пикселей, которая может быть количеством сеток или объектом с другими свойствами | число|объект | - |
lg | Адаптивная сетка ≥992 пикселей, которая может быть количеством сеток или объектом с другими свойствами | число|объект | - |
xl | Адаптивная сетка ≥1200 пикселей, которая может быть количеством сеток или объектом с другими свойствами | число|объект | - |
xxl | ≥1600px Отзывчивая сетка, может быть растровым номером или объектом, содержащим другие свойства | число|объект | - |
Продемонстрируйте свои навыки
Эта практика в основном знакомит с практикой React Hooks + TypeScript, не углубляясь в CSS.
Шаг 1. Определите тип Prop для компонента Row в соответствии с API.
// Row.tsx
+ import React, { CSSProperties, ReactNode } from 'react';
+ import import ClassNames from 'classnames';
+
+ type gutter = number | [number, number];
+ type align = 'top' | 'middle' | 'bottom';
+ type justify = 'start' | 'end' | 'center' | 'space-around' | 'space-between';
+
+ interface RowProps {
+ className?: string;
+ align?: align;
+ justify?: justify;
+ gutter?: gutter;
+ style?: CSSProperties;
+ children?: ReactNode;
+ }
Здесь мы используем основные типы данных, типы объединения и интерфейсы, предоставляемые TypeScript.
базовый тип данныхСуществует два типа JavaScript:原始数据类型(Primitive data types
)а также对象类型(Object types)
.
К примитивным типам данных относятся:布尔值
,数值
,字符串
,null
,undefined
и новые типы в ES6Symbol
. В основном мы представляем применение первых пяти примитивных типов данных в TypeScript.
тип союзаТипы объединения указывают, что значение может быть одним из нескольких типов.
введите псевдонимПсевдонимы типов используются для присвоения типу нового имени.
интерфейсИнтерфейс — очень гибкая концепция в TypeScript, которая не только абстрагирует часть поведения класса, но и часто используется для описания формы объекта. Здесь мы описываем RowProps с использованием интерфейса.
Шаг 2. Напишите базовый скелет компонента Row.
// Row.tsx
- import React, { CSSProperties, ReactNode } from 'react';
+ import React, { CSSProperties, ReactNode, FC } from 'react';
import ClassNames from 'classnames';
type gutter = number | [number, number];
type align = 'top' | 'middle' | 'bottom';
type justify = 'start' | 'end' | 'center' | 'space-around' | 'space-between';
interface RowProps {
// ...
}
+ const Row: FC<RowProps> = props => {
+ const { className, align, justify, children, style = {} } = props;
+ const classes = ClassNames('azir-row', className, {
+ [`azir-row-${align}`]: align,
+ [`azir-row-${justify}`]: justify
+ });
+
+ return (
+ <div className={classes} style={style}>
+ {children}
+ </div>
+ );
+ };
+ Row.defaultProps = {
+ align: 'top',
+ justify: 'start',
+ gutter: 0
+ };
+ export default Row;
Здесь мы используемДженерики, так что же такое дженерики?
ДженерикиОбобщения (Generics) относятся к функции, которая не указывает конкретный тип заранее при определении функции, интерфейса или класса, но указывает тип при его использовании.
function loggingIdentity<T>(arg: T): T {
return arg;
}
Шаг 3. Определите тип Prop для компонента Col в соответствии с API.
// Col.tsx
+ import React, {ReactNode, CSSProperties } from 'react';
+ import ClassNames from 'classnames';
+
+ interface ColCSSProps {
+ offset?: number;
+ order?: number;
+ pull?: number;
+ push?: number;
+ span?: number;
+ }
+
+ export interface ColProps {
+ className?: string;
+ style?: CSSProperties;
+ children?: ReactNode;
+ flex?: string | number;
+ offset?: number;
+ order?: number;
+ pull?: number;
+ push?: number;
+ span?: number;
+ xs?: ColCSSProps;
+ sm?: ColCSSProps;
+ md?: ColCSSProps;
+ lg?: ColCSSProps;
+ xl?: ColCSSProps;
+ xxl?: ColCSSProps;
+ }
Шаг 4. Напишите базовый скелет компонента Col.
// Col.tsx
import React, {ReactNode, CSSProperties } from 'react';
import ClassNames from 'classnames';
interface ColCSSProps {
// ...
}
export interface ColProps {
// ...
}
+ type mediaScreen = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
+ function sc(size: mediaScreen, value: ColCSSProps): Array<string> {
+ const t: Array<string> = [];
+ Object.keys(value).forEach(key => {
+ t.push(`azir-col-${size}-${key}-${value[key]}`);
+ });
+ return t;
+ }
+ const Col: FC<ColProps> = props => {
+ const {
+ className,
+ style = {},
+ span,
+ offset,
+ children,
+ pull,
+ push,
+ order,
+ xs,
+ sm,
+ md,
+ lg,
+ xl,
+ xxl
+ } = props;
+
+ const [classes, setClasses] = useState<string>(
+ ClassNames('azir-col', className, {
+ [`azir-col-span-${span}`]: span,
+ [`azir-col-offset-${offset}`]: offset,
+ [`azir-col-pull-${pull}`]: pull,
+ [`azir-col-push-${push}`]: push,
+ [`azir-col-order-${order}`]: order
+ })
+ );
+
+ // 响应式 xs,sm,md,lg,xl,xxl
+ useEffect(() => {
+ xs && setClasses(classes => ClassNames(classes, sc('xs', xs)));
+ sm && setClasses(classes => ClassNames(classes, sc('sm', sm)));
+ md && setClasses(classes => ClassNames(classes, sc('md', md)));
+ lg && setClasses(classes => ClassNames(classes, sc('lg', lg)));
+ xl && setClasses(classes => ClassNames(classes, sc('xl', xl)));
+ xxl && setClasses(classes => ClassNames(classes, sc('xxl', xxl)));
+ }, [xs, sm, md, lg, xl, xxl]);
+
+ return (
+ <div className={classes} style={style}>
+ {children}
+ </div>
+ );
+ };
+ Col.defaultProps = {
+ offset: 0,
+ pull: 0,
+ push: 0,
+ span: 24
+ };
+ Col.displayName = 'Col';
+
+ export default Col;
это здесьTypeScript
Компилятор выдал предупреждение.
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'ColCSSProps'.
No index signature with a parameter of type 'string' was found on type 'ColCSSProps'. TS7053
71 | const t: Array<string> = [];
72 | Object.keys(value).forEach(key => {
> 73 | t.push(`azir-col-${size}-${key}-${value[key]}`);
| ^
74 | });
75 | return t;
76 | }
Это означает: Элементы неявно имеютany
тип, типstring
нельзя использовать дляColCSSProps
индексный тип. Так чем же эта проблема закончится?
interface ColCSSProps {
offset?: number;
order?: number;
pull?: number;
push?: number;
span?: number;
+ [key: string]: number | undefined;
}
нам просто нужно сказатьTypeScript
ColCSSProps
Ключевой типstring
Тип значенияnumber | undefined
Вот и все.
тестовое задание
Теперь, когда он написан, пришло время протестировать код.
// example.tsx
import React from 'react';
import Row from './row';
import Col from './col';
export default () => {
return (
<div data-test="row-test" style={{ padding: '20px' }}>
<Row className="jd-share">
<Col style={{ background: 'red' }} span={2}>
123
</Col>
<Col style={{ background: 'yellow' }} offset={2} span={4}>
123
</Col>
<Col style={{ background: 'blue' }} span={6}>
123
</Col>
</Row>
<Row>
<Col order={1} span={8} xs={{ span: 20 }} lg={{ span: 11, offset: 1 }}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col1
</div>
</Col>
<Col span={4} xs={{ span: 4 }} lg={{ span: 12 }}>
<div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col2</div>
</Col>
</Row>
</div>
);
};
под размер экрана xs
lg размер экранаПока эффект очень хороший.
Шаг 5. Ограничьте дочерний компонент строки
Хотя эффект хорошийRow
компонентChildren
Любой элемент может быть передан
// row.tsx
const Row: FC<RowProps> = props => {
// ...
return (
<div className={classes} style={style}>
{children}
</div>
);
};
Это слишком случайно! еслиChildren
содержит неCol
Компоновка узла сборки, потом будут проблемы, решил ограничиться здесьRow
компонентChildren
Типы.
Так как его ограничить? Некоторые люди подумают, что прямоеchildren.map
, А по строению нельзя судить? Делать это нежелательно, т.React
Чиновники также отмечают, чтоchildren
позвонить прямо наmap
очень опасно, потому что мы не можем быть увереныchildren
тип. Так что делать?React
Официальный очень внимательный, а также предоставляет нам APIReact.Children
Перед этим мы даемCol
Компонент устанавливает встроенное свойствоdisplayName
свойства, которые помогут нам определить тип.
// col.tsx
const Col: FC<ColProps> = props => {
// ...
};
// ...
+ Col.displayName = 'Col';
Тогда мы приглашаем, потому что старший братReact.Children
API. этоAPI
может быть специально использован для обработкиChildren
. Давайте напишем компонент RowrenderChildren
функция
// row.tsx
const Row: FC<RowProps> = props => {
const { className, align, justify, children, style = {} } = props;
const classes = ClassNames('azir-row', className, {
[`azir-row-${align}`]: align,
[`azir-row-${justify}`]: justify
});
+ const renderChildren = useCallback(() => {
+ return React.Children.map(children, (child, index) => {
+ try {
+ // child 是 ReactNode 类型,在该类型下有很多子类型,我们需要断言一下
+ const childElement = child as React.FunctionComponentElement<ColProps>;
+ const { displayName } = childElement.type;
+ if (displayName === 'Col') {
+ return child;
+ } else {
+ console.error(
+ 'Warning: Row has a child which is not a Col component'
+ );
+ }
+ } catch (e) {
+ console.error('Warning: Row has a child which is not a Col component');
+ }
+ });
+ }, [children]);
return (
<div className={classes} style={style}>
- {children}
+ {renderChildren()}
</div>
);
};
Мы уже сделали 80%, мы что-то забыли? ? ?
Шаг 6 Вишенка на торте — желоб
мы проходим外层 margin
+ 内层 padding
в соответствии с настройками интервала по горизонтали и вертикали.
// row.tsx
import React, {
CSSProperties, ReactNode, FC, FunctionComponentElement, useCallback, useEffect, useState
} from 'react';
// ...
const Row: FC<RowProps> = props => {
- const { className, align, justify, children, style = {} } = props;
+ const { className, align, justify, children, gutter, style = {} } = props;
+ const [rowStyle, setRowStyle] = useState<CSSProperties>(style);
// ...
return (
- <div className={classes} style={style}>
+ <div className={classes} style={rowStyle}>
{renderChildren()}
</div>
);
};
// ...
export default Row;
Row
компонентmargin
Это уже установлено, тогдаCol
компонентpadding
Что мы можем сделать по этому поводу? Есть два пути, один - пройтиprops
, второй - использоватьcontext
, я решил использовать контекст для связи компонентов, потому что я не хотел делать свойства компонента Col слишком беспорядочными (это достаточно беспорядочно...).
// row.tsx
import React, {
CSSProperties, ReactNode, FC, FunctionComponentElement, useCallback, useEffect, useState
} from 'react';
// ...
export interface RowContext {
gutter?: gutter;
}
export const RowContext = createContext<RowContext>({});
const Row: FC<RowProps> = props => {
- const { className, align, justify, children, style = {} } = props;
+ const { className, align, justify, children, gutter, style = {} } = props;
+ const [rowStyle, setRowStyle] = useState<CSSProperties>(style);
+ const passedContext: RowContext = {
+ gutter
+ };
// ...
return (
<div className={classes} style={rowStyle}>
+ <RowContext.Provider value={passedContext}>
{renderChildren()}
+ </RowContext.Provider>
</div>
);
};
// ...
export default Row;
мы вRow
компонент создалcontext
, далее вCol
используемые компоненты и рассчитанныеCol
компонентыgutter
соответствующийpadding
стоимость.
// col.tsx
import React, {
ReactNode,
CSSProperties,
FC,
useState,
useEffect,
+ useContext
} from 'react';
import ClassNames from 'classnames';
+ import { RowContext } from './row';
// ...
const Col: FC<ColProps> = props => {
// ...
+ const [colStyle, setColStyle] = useState<CSSProperties>(style);
+ const { gutter } = useContext(RowContext);
+ // 水平垂直间距
+ useEffect(() => {
+ if (Object.prototype.toString.call(gutter) === '[object Number]') {
+ const padding = gutter as number;
+ if (padding >= 0) {
+ setColStyle(style => ({
+ padding: `${padding / 2}px`,
+ ...style
+ }));
+ }
+ }
+ if (Object.prototype.toString.call(gutter) === '[object Array]') {
+ const [paddingX, paddingY] = gutter as [number, number];
+ if (paddingX >= 0 && paddingY >= 0) {
+ setColStyle(style => ({
+ padding: `${paddingY / 2}px ${paddingX / 2}px`,
+ ...style
+ }));
+ }
+ }
+ }, [gutter]);
// ...
return (
- <div className={classes} style={style}>
+ <div className={classes} style={colStyle}>
{children}
</div>
);
};
// ...
export default Col;
На данный момент наш компонент сетки готов! Давайте проверим это! 😄
тестовое задание
import React from 'react';
import Row from './row';
import Col from './col';
export default () => {
return (
<div data-test="row-test" style={{ padding: '20px' }}>
<Row>
<Col span={24}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col1
</div>
</Col>
</Row>
<Row gutter={10}>
<Col order={1} span={8} xs={{ span: 20 }} lg={{ span: 11, offset: 1 }}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col1
</div>
</Col>
<Col span={4} xs={{ span: 4 }} lg={{ span: 12 }}>
<div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col2</div>
</Col>
</Row>
<Row gutter={10} align="middle">
<Col span={8}>
<div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col1</div>
</Col>
<Col offset={8} span={8}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col2
</div>
</Col>
</Row>
<Row gutter={10} align="bottom">
<Col span={4}>
<div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col1</div>
</Col>
<Col span={8}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col2
</div>
</Col>
<Col push={3} span={9}>
<div style={{ height: '130px', backgroundColor: '#2170bb' }}>
Col3
</div>
</Col>
<Col span={4}>
<div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col1</div>
</Col>
<Col span={8}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col2
</div>
</Col>
<Col span={8}>
<div style={{ height: '130px', backgroundColor: '#2170bb' }}>
Col3
</div>
</Col>
<Col pull={1} span={3}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col2
</div>
</Col>
</Row>
</div>
);
};
Суммировать
слишком далекоReact Hooks + TypeScript
Обмен практикой окончен, я только перечисляю наиболее часто используемыеHooks API
а такжеTypeScript
Характеристики воробьев, хоть и маленькие и полные, мы уже можем испытатьReact Hooks + TypeScript
Преимущества, которые дают эти два компонента, определенно сделают наш код легким и надежным. оHooks
а такжеTypeScript
Содержание, я надеюсь, читатели перейдут на официальный сайт для более глубокого изучения.