React Hooks + боевой рекорд TypeScript

React.js

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

  1. ReactПредположим, когда мы звоним несколько разuseState, убедитесь, что порядок их вызова одинаков при каждом рендеринге.
  2. Добавьте некоторые внутренние элементы в компонент, вызвав его в функциональном компоненте.state,Reactсохранит это состояние при повторном рендеринге
  3. useStateЕдинственным параметром является начальныйstate
  4. useStateвернет массив: astate, обновлениеstateФункция
  5. Во время первоначального рендеринга возвращаемое состояние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

  • TypeScript повышает удобочитаемость и ремонтопригодность кода
    • Система типов на самом деле является лучшей документацией, большинство функций можно использовать, просто взглянув на определения типов.
    • Большинство ошибок можно найти во время компиляции, что лучше, чем ошибки во время выполнения.
    • Расширьте возможности редактора и IDE, включая завершение кода, подсказку интерфейса, переход к определению, реконструкцию и т. д.
  • TypeScript очень инклюзивен
    • TypeScript — это надмножество JavaScript, файлы .js можно напрямую переименовывать в .ts.
    • Автоматический вывод типа даже без явного определения типа
    • Может определить практически любой тип от простого до сложного
    • Создавайте файлы JavaScript, даже если TypeScript компилирует ошибки
    • Совместим со сторонними библиотеками, даже если сторонняя библиотека не написана на TypeScript, вы можете написать отдельный файл типа для чтения TypeScript
  • TypeScript имеет активное сообщество
    • В большинстве сторонних библиотек есть файлы определения типов для TypeScript.
    • TypeScript включает в себя спецификацию ES6, а также поддерживает некоторые черновые спецификации ESNext.

Зная React Hooks и TypeScript, давайте взглянем на их комбинацию вместе! 😄

упражняться

Эта практика исходит из проекта библиотеки компонентов с открытым исходным кодом, который я разрабатываю.Azir DesignсерединаGridКомпонент компоновки сетки.

Цель

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 尺寸屏幕下
lg размер экрана

xs 尺寸屏幕下

Пока эффект очень хороший.

Шаг 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.ChildrenAPI. это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Содержание, я надеюсь, читатели перейдут на официальный сайт для более глубокого изучения.