7 принципов надежного проектирования компонентов React Pure Components

JavaScript React.js
7 принципов надежного проектирования компонентов React Pure Components

Перевод: Лю Сяоси

Оригинальная ссылка:Рисовое путешествие avlutin.com/7-architect…

длина оригиналаОченьОно длинное, но содержание слишком привлекательно для меня, поэтому я не могу не перевести его. Эта статья очень полезна для написания повторно используемых и поддерживаемых компонентов React. Но поскольку длина слишком велика, я разделил статью, эта статья посвященаЧистые и почти чистые компоненты. Из-за ограниченного уровня некоторые переводы в тексте могут быть недостаточно точными, если у вас есть идеи получше, укажите их в комментариях.

Более качественные изделия можно штамповать: GitHub.com/Иветт Л.А. Ю/Б…

——————————————— Я — разделительная линия ————————————————

Чистые и почти чистые компоненты

Чистые компоненты всегда отображают один и тот же элемент для одного и того же значения свойства. Почти чистые компоненты всегда отображают один и тот же элемент для одного и того же значения свойства, но имеют побочные эффекты.

В функциональном программировании чистая функция всегда возвращает одни и те же выходные данные при одних и тех же входных данных и не оказывает побочных эффектов на внешний мир.

function sum(a, b) {
    return a + b;
}
sum(5, 10); // => 15

Для заданных двух чиселsum()Функция всегда будет возвращать один и тот же результат.

Когда функция имеет одинаковые входные данные и разные выходные данные, это не чистая функция. Когда функция зависит от глобального состояния, это не чистая функция, например:

let said = false;

function sayOnce(message) {
    if (said) {
        return null;
    }
    said = true;
    return message;
}

sayOnce('Hello World!'); // => 'Hello World!'
sayOnce('Hello World!'); // => null

sayOnce('Hello World!')При первом звонке вернутьсяHello World.

Даже если одинаковые входные параметры, являютсяHello World, но второй вызовsayOnce('Hello World!'), возвращаемый результатnull. Вот нечистая функцияхарактерная черта: зависит от глобального состоянияsaid

sayOnce()тело функции,said = trueИзменение глобального состояния имеет побочные эффекты для внешнего мира, что также является одной из характеристик нечистых функций.

С другой стороны, чистые функции не имеют побочных эффектов и не зависят от глобального состояния. Пока вход тот же, выход должен быть таким же. Поэтому результаты чистых функций предсказуемы, детерминированы, многократно используются и легко проверяются.

ReactКомпоненты также следует считать чистыми компонентами, когдаpropКогда значения одинаковы, чистые компоненты (обратите внимание на различиеReact.PureComponent) отображает тот же контент, давайте посмотрим на пример:

function Message({ text }) {
    return <div className="message">{text}</div>;
}

<Message text="Hello World!" />
// => <div class="message">Hello World</div>

когда перешел кMessageизpropКогда значение одинаковое, он отображает тот же элемент.

Невозможно гарантировать, что все компоненты являются чистыми компонентами, иногда вам нужно знать, как взаимодействовать с внешним миром, например, в следующем примере:

class InputField extends Component {
    constructor(props) {
        super(props);
        this.state = { value: '' };
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange({ target: { value } }) {
        this.setState({ value });
    }

    render() {
        return (
            <div>
                <input
                    type="text"
                    value={this.state.value}
                    onChange={this.handleChange}
                />
                You typed: {this.state.value}
            </div>
        );
    }
}

<InputField>компонентов, не принимает никакихprops, но вместо этого отображает вывод на основе ввода пользователя.<InputField>Должен быть нечистым компонентом, потому что он должен пройтиinputПоле ввода взаимодействует с внешним миром.

Нечистые компоненты необходимы, глобальное состояние, сетевые запросы, локальное хранилище и т. д. требуются в большинстве приложений. Все, что вы можете сделать, это положить Чистые компоненты и нечистые компоненты изолированы, то есть ваши компонентыОчистить.

Нечистый код явно говорит, что он имеет побочные эффекты или полагается на глобальное состояние. Сам по себе нечистый код оказывает менее непредсказуемое влияние на остальную часть системы.

Рассмотрим подробно пример очистки.

Практический пример: извлечение чистых компонентов из глобальных переменных

Мне не нравятся глобальные переменные, потому что они нарушают инкапсуляцию, создают непредсказуемое поведение и затрудняют тестирование.

Глобальные переменные могут использоваться как изменяемые или неизменяемые объекты.

Переменные глобальные переменные затрудняют управление поведением компонентов, а данные можно вводить и изменять по желанию, влияя накоординацияпроцесс, что явно неправильно.

Если вам нужно изменяемое глобальное состояние, вы можете рассмотреть возможность использованияReduxдля управления состоянием вашего приложения.

Неизменяемые глобальные переменные обычно представляют собой объекты конфигурации приложения, которые содержат имена сайтов, имена пользователей для входа в систему или другую информацию о конфигурации.

Следующий код определяет объект конфигурации, содержащий имя сайта:

export const globalConfig = {
    siteName: 'Animals in Zoo'
};

<Header>Компонент отображает заголовок приложения, включая отображаемое имя сайта:Animals in Zoo

import { globalConfig } from './config';

export default function Header({ children }) {
    const heading =
        globalConfig.siteName ? <h1>{globalConfig.siteName}</h1> : null;
    return (
        <div>
            {heading}
            {children}
        </div>
    );
}

<Header>Использование компонентовglobalConfig.siteNameдля отображения имени сайта, когдаglobalConfig.siteNameКогда он не определен, он не отображается.

Первое, что нужно отметить, это то, что<Header>является нечистым компонентом. даже если входящийchildrenодно и то же значение, также потому, чтоglobalConfig.siteNameРазные значения возвращают разные результаты.

// globalConfig.siteName is 'Animals in Zoo'
<Header>Some content</Header>
    // Renders:
    <div>
        <h1>Animals in Zoo</h1>
        Some content
</div>

или:

// globalConfig.siteName is `null`
<Header>Some content</Header>
    // Renders:
    <div>
        Some content
</div>

Во-вторых, становится трудным тестирование, чтобы проверить, как компонент обрабатывает имя сайта.null, мы должны вручную установитьglobalConfig.siteName = null

import assert from 'assert';
import { shallow } from 'enzyme';
import { globalConfig } from './config';
import Header from './Header';

describe('<Header />', function () {
    it('should render the heading', function () {
        const wrapper = shallow(
            <Header>Some content</Header>
        );
        assert(wrapper.contains(<h1>Animals in Zoo</h1>));
    });

    it('should not render the heading', function () {
        // Modification of global variable:
        globalConfig.siteName = null;
        const wrapper = shallow(
            <Header>Some content</Header>
        );
        assert(appWithHeading.find('h1').length === 0);
    });
});

Модифицировано для тестированияglobalConfig.siteName = nullнеудобно. Это происходит потому, что<Heading>Существует сильная зависимость от глобальных переменных.

Чтобы решить эту проблему, глобальные переменные можно использовать в качестве входных данных для компонента вместо того, чтобы внедрять их в область действия компонента.

Давайте изменим его<Header>компонент так, чтобы он принял еще одинsiteNmaeизprop, а затем используйтеrecomposeв библиотекеdefaultPropsКомпоненты более высокого порядка для обертывания компонентов,defaultPropsГарантируется отсутствие входящегоprops, используйте значение по умолчанию.

import { defaultProps } from 'recompose';
import { globalConfig } from './config';

export function Header({ children, siteName }) {
    const heading = siteName ? <h1>{siteName}</h1> : null;
    return (
        <div className="header">
            {heading}
            {children}
        </div>
    );
}

export default defaultProps({
    siteName: globalConfig.siteName
})(Header);

<Header>Становится чистой функциональной композицией, больше не зависящей напрямуюglobalConfigпеременные для облегчения тестирования.

В то же время, когда мы не устанавливаемsiteNameчас,defaultPropsбудет передан вglobalConfig.siteNameтак какsiteNameстоимость имущества. Это где примеси кода разделены и изолированы от.

Теперь давайте протестируем чистую версию<Header>Компоненты:

import assert from 'assert';
import { shallow } from 'enzyme';
import { Header } from './Header'; // Import the pure Header

describe('<Header />', function () {
    it('should render the heading', function () {
        const wrapper = shallow(
            <Header siteName="Animals in Zoo">Some content</Header>
        );
        assert(wrapper.contains(<h1>Animals in Zoo</h1>));
    });

    it('should not render the heading', function () {
        const wrapper = shallow(
            <Header siteName={null}>Some content</Header>
        );
        assert(appWithHeading.find('h1').length === 0);
    });
});

Ну а теперь тестирование чистых компонентов<Header>Простой. Тесты делают одну вещь: проверяют, что компонент отображает ожидаемый элемент с учетом входных данных. Нет необходимости импортировать, получать доступ или изменять глобальные переменные, никаких побочных эффектов. Хорошо спроектированные компоненты легко тестировать.

Практический пример: извлечение чистых компонентов из сетевых запросов

рассмотрение<WeatherFetch>компонент, когда он монтируется, он делает сетевые запросы для получения информации о погоде.

class WeatherFetch extends Component {
    constructor(props) {
        super(props);
        this.state = { temperature: 'N/A', windSpeed: 'N/A' };
    }

    render() {
        const { temperature, windSpeed } = this.state;
        return (
            <WeatherInfo temperature={temperature} windSpeed={windSpeed} />
        );
    }

    componentDidMount() {
        axios.get('http://weather.com/api').then(function (response) {
            const { current } = response.data;
            this.setState({
                temperature: current.temperature,
                windSpeed: current.windSpeed
            })
        });
    }
}

<WeatherFetch>Это не чистый компонент, потому что одни и те же входные данные будут давать разные выходные данные, поскольку рендеринг компонента зависит от результата, возвращаемого сервером.

К сожалению,HTTPПобочные эффекты запроса не могут быть устранены,<WeatherFetch>В его обязанности входит запрос данных с сервера.

Но вы можете позволить<WeatherFetch>Сделать тот же контент для того же значения свойства. Это изолирует побочные эффекты дляpropФункциональные свойстваfetch()начальство. Такой тип компонента называется почти чистым компонентом.

Возьмем нечистый компонент<WeatherFetch>Переписан как почти чистый компонент.ReduxЭто может быть хорошим подспорьем для извлечения деталей реализации побочных эффектов из компонентов. Поэтому нам нужно установить некоторыеReduxСтруктура.

fetch() action createrЗапустите вызов сервера:

export function fetch() {
    return {
        type: 'FETCH'
    };
}

redux-sagaперехваченныйFetch action, на самом деле хотите, чтобы сервер запрашивал, когда запрос завершен, отправляетFETCH_SUCCESSизaction

import { call, put, takeEvery } from 'redux-saga/effects';

export default function* () {
    yield takeEvery('FETCH', function* () {
        const response = yield call(axios.get, 'http://weather.com/api');
        const { temperature, windSpeed } = response.data.current;
        yield put({
            type: 'FETCH_SUCCESS',
            temperature,
            windSpeed
        });
    });
}

этоreducerОтвечает за обновление состояния приложения.

const initialState = { temperature: 'N/A', windSpeed: 'N/A' };

export default function (state = initialState, action) {
    switch (action.type) {
        case 'FETCH_SUCCESS':
            return {
                ...state,
                temperature: action.temperature,
                windSpeed: action.windSpeed
            };
        default:
            return state;
    }
}

пс: опущено для простотыRedux storeа такжеsagasинициализация.

несмотря на использованиеReduxТребуются дополнительные структуры, такие как:actions ,reducersа такжеsagas, но это помогает сделать<WeatherFetch>становятся почти чистыми компонентами.

Давайте изменим его<WeatherFetch>, сделать это иReduxКомбинированный.

import { connect } from 'react-redux';
import { fetch } from './action';

export class WeatherFetch extends Component {
    render() {
        const { temperature, windSpeed } = this.props;
        return (
            <WeatherInfo temperature={temperature} windSpeed={windSpeed} />
        );
    }

    componentDidMount() {
        this.props.fetch();
    }
}

function mapStateToProps(state) {
    return {
        temperature: state.temperate,
        windSpeed: state.windSpeed
    };
}
export default connect(mapStateToProps, { fetch });

connect(mapStateToProps, { fetch })HOC в упаковке<WeatherFetch>.

Когда компонент монтируется,action creator this.props.fetch()Вызывается для запуска запроса сервера, когда запрос завершен,Reduxобновить приложениеstate, так что<WeatherFetch>отpropsполучить вtemperatureа такжеwindSpeed.

this.props.fetchсостоит в том, чтобы изолировать нечистый код, вызывающий побочные эффекты. потому чтоReduxСуществование компонента больше не нужно использовать внутри компонентаaxoisбиблиотека, просьбаURLили обработкаpromise. Кроме того, новая версия<WeatherFetch>будет то же самоеpropsvalue отображает тот же элемент. Этот компонент становится почти чистым компонентом.

Протестируйте почти чистую версию по сравнению с нечистой версией<WeatherFetch>намного легче:

import assert from 'assert';
import { shallow, mount } from 'enzyme';
import { spy } from 'sinon';
// Import the almost-pure version WeatherFetch
import { WeatherFetch } from './WeatherFetch';
import WeatherInfo from './WeatherInfo';

describe('<WeatherFetch />', function () {
    it('should render the weather info', function () {
        function noop() { }
        const wrapper = shallow(
            <WeatherFetch temperature="30" windSpeed="10" fetch={noop} />
        );
        assert(wrapper.contains(
            <WeatherInfo temperature="30" windSpeed="10" />
        ));
    });

    it('should fetch weather when mounted', function () {
        const fetchSpy = spy();
        const wrapper = mount(
            <WeatherFetch temperature="30" windSpeed="10" fetch={fetchSpy} />
        );
        assert(fetchSpy.calledOnce);
    });
});

Вам нужно проверить, учитываяpropстоимость,<WeatherFetch>Является ли результат рендеринга ожидаемым и вызывается при монтированииfetch(). Просто и понятно.

Преобразование почти чистых компонентов в чистые компоненты

По сути, на этом шаге уже не нужно отделять нечистый код, почти чистые компоненты предсказуемы и легко тестируются.

Но... посмотрим, насколько глубока кроличья нора. почти чистая версия<WeatherFetch>Компонент может быть преобразован в идеальный чистый компонент.

мы придемfetchОбратный вызов извлечен вrecomposeбиблиотекаlifecycle()в компонентах более высокого порядка.

import { connect } from 'react-redux';
import { compose, lifecycle } from 'recompose';
import { fetch } from './action';

export function WeatherFetch({ temperature, windSpeed }) {
    return (
        <WeatherInfo temperature={temperature} windSpeed={windSpeed} />
    );
}

function mapStateToProps(state) {
    return {
        temperature: state.temperate,
        windSpeed: state.windSpeed
    };
}

export default compose(
    connect(mapStateToProps, { fetch }),
    lifecycle({
        componentDidMount() {
            this.props.fetch();
        }
    })
)(WeatherFetch);

lifecycle()Компоненты более высокого порядка принимают объект с помощью методов жизненного цикла. передачаthis.props.fecth()методcomponentDidMount()обрабатывается компонентами более высокого порядка, устраняя побочные эффекты от<WeatherFetch>извлеченный из.

Сейчас,<WeatherFetch>является чистым компонентом, он больше не имеет побочных эффектов, а когда значение входного свойстваtemperatureа такжеwindSpeedКогда то же самое, вывод всегда один и тот же.

Хотя чистая версия<WeatherFetch>Хорошо для предсказуемости и понимания вещей, но для этого нужно что-то вродеcompose(),lifecycle()И другие компоненты высшего порядка, поэтому, в целом, следует ли преобразовать практически чистые компоненты в чистые компоненты для нас.

Наконец, спасибо за вашу готовность потратить свое драгоценное время на чтение этой статьи. Если эта статья дала вам небольшую помощь или вдохновение, пожалуйста, не скупитесь на лайки и звезды. двигаться вперед.GitHub.com/Иветт Л.А. Ю/Б…

Подпишитесь на официальный аккаунт и присоединитесь к группе технического обмена