Како вам „Златно правило“ компонената Реацт може помоћи да напишете бољи код

И како куке улазе у игру

Недавно сам усвојио нову филозофију која мења начин на који израђујем компоненте. То није нужно нова идеја, већ пре суптилни нови начин размишљања.

Златно правило компонената

Створите и дефинишите компоненте на најприроднији начин, узимајући у обзир само оно што им је потребно за функционисање.

Опет, то је суптилна изјава и можда мислите да је већ следите, али лако је противити се овоме.

На пример, рецимо да имате следећу компоненту:

Да дефинишете ову компоненту „природно“, вероватно бисте је написали са следећим АПИ-јем:

PersonCard.propTypes = { name: PropTypes.string.isRequired, jobTitle: PropTypes.string.isRequired, pictureUrl: PropTypes.string.isRequired, };

Што је прилично једноставно - само гледајући шта треба да би функционисало, потребно вам је само име, наслов посла и УРЛ слике.

Али рецимо да имате захтев да прикажете „званичну“ слику у зависности од корисничких подешавања. Можда ћете доћи у искушење да напишете АПИ овако:

PersonCard.propTypes = { name: PropTypes.string.isRequired, jobTitle: PropTypes.string.isRequired, officialPictureUrl: PropTypes.string.isRequired, pictureUrl: PropTypes.string.isRequired, preferOfficial: PropTypes.boolean.isRequired, };

Можда се чини да компоненти требају додатни реквизити да би функционисали, али заправо компонента не изгледа другачије и не требају јој ти додатни реквизити да би функционисала. Оно што ови додатни реквизити раде је спајање ове preferOfficialпоставке са вашом компонентом и чини свако коришћење компоненте изван тог контекста заиста неприродним.

Премостити јаз

Па ако логика за пребацивање УРЛ-а слике не припада самој компоненти, где припада?

Шта кажете на indexдатотеку?

Усвојили смо структуру директоријума где свака компонента прелази у истоимени директоријум где је indexдатотека одговорна за премошћавање јаза између ваше „природне“ компоненте и спољног света. Ову датотеку називамо „контејнер“ (надахнут концептом компонената „контејнера“ компаније Реацт Редук).

/PersonCard -PersonCard.js ------ the "natural" component -index.js ----------- the "container"

Контејнере дефинишемо као део кода који премошћује јаз између ваше природне компоненте и спољног света. Из тог разлога те ствари понекад називамо и „ињекторима“.

Ваша природна компонента је код који бисте креирали да вам се покаже само слика онога што се од вас захтева (без детаља о томе како бисте добили податке или где би били смештени у апликацију - све што знате је да треба да функционише).

Спољашњи свет је кључна реч ћемо користити да се односе на било који ресурс ваша апликација има (нпр редук продавници) који може да се трансформише у задовољи реквизите ваше природне компоненте.

Циљ овог чланка: Како можемо да одржимо компоненте „природним“, а да их не загађујемо смећем из спољног света? Зашто је то боље?

Напомена: Иако инспирисани Дановим Абрамовим и термином Реацт Редук, наша дефиниција „контејнера“ иде мало даље од тога и суптилно се разликује. Једина разлика између контејнера Дана Абрамова и нашег је само на концептуалном нивоу. Дан'с каже да постоје две врсте компонената: презентационе компоненте и компоненте контејнера. Правимо овај корак даље и кажемо да постоје компоненте, а затим контејнери. Иако имплементирамо контејнере са компонентама, не мислимо на контејнере као компоненте на концептуалном нивоу. Због тога препоручујемо да свој спремник ставите у indexдатотеку - јер је то мост између ваше природне компоненте и спољног света и не стоји самостално.

Иако је овај чланак усредсређен на компоненте, већи део овог чланка чине контејнери.

Зашто?

Израда природних компонената - Једноставно, чак и забавно.

Повезивање компонената са спољним светом - Нешто теже.

Како ја видим, постоје три главна разлога због којих бисте загађивали своју природну компоненту смећем из спољног света:

  1. Чудне структуре података
  2. Захтеви ван опсега компоненте (као пример изнад)
  3. Активирање догађаја на исправкама или на монтирању

Следећих неколико одељака покушаће да покрију ове ситуације примерима са различитим врстама примена контејнера.

Рад са чудним структурама података

Понекад да бисте пружили потребне информације, требате повезати податке и трансформисати их у нешто што је разумније. У недостатку боље речи, „чудне“ структуре података су једноставно структуре података које су неприродне за употребу ваше компоненте.

Веома је примамљиво чудне структуре података проследити директно у компоненту и извршити трансформацију унутар саме компоненте, али то доводи до збуњујућих и често тешких компонената за тестирање.

Ухватио сам се да сам недавно упао у ову замку када сам добио задатак да створим компоненту која своје податке добија из одређене структуре података коју користимо за подршку одређеној врсти обрасца.

ChipField.propTypes = { field: PropTypes.object.isRequired, // <-- the "weird" data structure onEditField: PropTypes.func.isRequired, // <-- and a weird event too };

Компонента је ову чудну fieldструктуру података узела као потпору. Практично, ово би можда било у реду да никад више не бисмо морали додирнути ствар, али то је постало прави проблем када смо тражили да је поново користимо на другом месту које није повезано са овом структуром података.

Будући да је компонента захтевала ову структуру података, било је немогуће поново је користити, а збуњивало је рефакторирање. Тестови које смо првобитно написали такође су били збуњујући јер су се ругали овој чудној структури података. Имали смо проблема са разумевањем тестова и проблемом њиховог поновног писања када смо на крају рефакторирали.

Нажалост, чудне структуре података су неизбежне, али употреба контејнера је одличан начин да се са њима изађе на крај. Овде се може узети нешто што вам архитектура компонената на овај начин даје могућност издвајања и градације компоненте у компоненту која се може поново употребити. Ако чудну структуру података пренесете у компоненту, изгубите ту опцију.

Напомена: Не предлажем да све компоненте које направите треба да буду генеричке од почетка. Предлог је да размислите о томе шта ваша компонента ради на основном нивоу, а затим премостите празнину. Као последица тога, ви сте вероватно да имају могућност да дипломирају своју компоненту у неутрошив један са минималним радом.

Имплементација контејнера помоћу функционалних компоненти

Ако строго мапирате реквизите, једноставна опција имплементације је употреба друге функционалне компоненте:

import React from 'react'; import PropTypes from 'prop-types'; import getValuesFromField from './helpers/getValuesFromField'; import transformValuesToField from './helpers/transformValuesToField'; import ChipField from './ChipField'; export default function ChipFieldContainer({ field, onEditField }) { const values = getValuesFromField(field); function handleOnChange(values) { onEditField(transformValuesToField(values)); } return ; } // external props ChipFieldContainer.propTypes = { field: PropTypes.object.isRequired, onEditField: PropTypes.func.isRequired, };

А структура директоријума за овакву компоненту изгледа отприлике овако:

/ChipField -ChipField.js ------------------ the "natural" chip field -ChipField.test.js -index.js ---------------------- the "container" -index.test.js /helpers ----------------------- a folder for the helpers/utils -getValuesFromField.js -getValuesFromField.test.js -transformValuesToField.js -transformValuesToField.test.js

Можда мислите „то је превише посла“ - а ако је тако, онда то схватам. Можда се чини да овде има још посла јер има више датотека и мало индиректности, али ево дела који вам недостаје:

import { connect } from 'react-redux'; import getPictureUrl from './helpers/getPictureUrl'; import PersonCard from './PersonCard'; const mapStateToProps = (state, ownProps) => { const { person } = ownProps; const { name, jobTitle, customPictureUrl, officialPictureUrl } = person; const { preferOfficial } = state.settings; const pictureUrl = getPictureUrl(preferOfficial, customPictureUrl, officialPictureUrl); return { name, jobTitle, pictureUrl }; }; const mapDispatchToProps = null; export default connect( mapStateToProps, mapDispatchToProps, )(PersonCard);

И даље се ради о истој количини посла без обзира да ли сте податке трансформисали изван компоненте или унутар ње. Разлика је у томе што када трансформишете податке изван компоненте, дајете себи експлицитније место да тестирате да ли су ваше трансформације тачне, а истовремено раздвајате проблеме.

Испуњавање захтева ван опсега компоненте

Попут горњег примера Персон Цард, врло је вероватно да када усвојите ово „златно правило“ размишљања схватићете да су одређени захтеви ван домета стварне компоненте. Па како то испунити?

Погађате: Контејнери?

Можете створити контејнере који чине мало додатног посла како би ваша компонента била природна. Када то учините, на крају ћете добити фокусиранију компоненту која је много једноставнија и спремник који је боље тестиран.

Let’s implement a PersonCard container to illustrate the example.

Implementing containers using higher order components

React Redux uses higher order components to implement containers that push and map props from the Redux store. Since we got this terminology from React Redux, it comes with no surprise that React Redux’s connect is a container.

Regardless if you’re using a function component to map props, or if you’re using higher order components to connect to the Redux store, the golden rule and the job of the container are still the same. First, write your natural component and then use the higher order component to bridge the gap.

Folder structure for above:

/PersonCard -PersonCard.js ----------------- natural component -PersonCard.test.js -index.js ---------------------- container -index.test.js /helpers -getPictureUrl.js ------------ helper -getPictureUrl.test.js
Note: In this case, it wouldn’t be too practical to have a helper for getPictureUrl. This logic was separated simply to show that you can. You also might’ve noticed that there is no difference in folder structure regardless of container implementation.

If you’ve used Redux before, the example above is something you’re probably already familiar with. Again, this golden rule isn’t necessarily a new idea but a subtle new way of thinking.

Additionally, when you implement containers with higher order components, you also have the ability to functionally compose higher order components together — passing props from one higher order component to the next. Historically, we’ve chained multiple higher order components together to implement a single container.

2019 Note: The React community seems to be moving away from higher order components as a pattern. I would also recommend the same. My experience when working with these is that they can be confusing for team members who aren’t familiar with functional composition and they can cause what is known as “wrapper hell” where components are wrapped too many times causing significant performance issues. Here are some related articles and resources on this: Hooks talk (2018) Recompose talk (2016) , Use a Render Prop! (2017), When to NOT use Render Props (2018).

You promised me hooks

Implementing containers using hooks

Why are hooks featured in this article? Because implementing containers becomes a lot easier with hooks.

If you’re not familiar with React hooks, then I would recommend watching Dan Abramov’s and Ryan Florence’s talks introducing the concept during React Conf 2018.

The gist is that hooks are the React team’s response to the issues with higher order components and similar patterns. React hooks are intended to be a superior replacement pattern for both in most cases.

This means that implementing containers can be done with a function component and hooks ?

In the example below, we’re using the hooks useRoute and useRedux to represent the “outside world” and we’re using the helper getValues to map the outside world into props usable by your natural component. We’re also using the helper transformValues to transform your component’s output to the outside world represented by dispatch.

import React from 'react'; import PropTypes from 'prop-types'; import { useRouter } from 'react-router'; import { useRedux } from 'react-redux'; import actionCreator from 'your-redux-stuff'; import getValues from './helpers/getVaules'; import transformValues from './helpers/transformValues'; import FooComponent from './FooComponent'; export default function FooComponentContainer(props) { // hooks const { match } = useRouter({ path: /* ... */ }); // NOTE: `useRedux` does not exist yet and probably won't look like this const { state, dispatch } = useRedux(); // mapping const props = getValues(state, match); function handleChange(e) { const transformed = transformValues(e); dispatch(actionCreator(transformed)); } // natural component return ; } FooComponentContainer.propTypes = { /* ... */ };

And here’s the reference folder structure:

/FooComponent ----------- the whole component for others to import -FooComponent.js ------ the "natural" part of the component -FooComponent.test.js -index.js ------------- the "container" that bridges the gap -index.js.test.js and provides dependencies /helpers -------------- isolated helpers that you can test easily -getValues.js -getValues.test.js -transformValues.js -transformValues.test.js

Firing events in containers

The last type of scenario where I find myself diverging from a natural component is when I need to fire events related to changing props or mounting components.

For example, let’s say you’re tasked with making a dashboard. The design team hands you a mockup of the dashboard and you transform that into a React component. You’re now at the point where you have to populate this dashboard with data.

You notice that you need to call a function (e.g. dispatch(fetchAction)) when your component mount in order for that to happen.

In scenarios like this, I found myself adding componentDidMount and componentDidUpdate lifecycle methods and adding onMount or onDashboardIdChanged props because I needed some event to fire in order to link my component to the outside world.

Following the golden rule, these onMount and onDashboardIdChanged props are unnatural and therefore should live in the container.

The nice thing about hooks is that it makes dispatching events onMount or on prop change much simpler!

Firing events on mount:

To fire an event on mount, call useEffect with an empty array.

import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { useRedux } from 'react-redux'; import fetchSomething_reduxAction from 'your-redux-stuff'; import getValues from './helpers/getVaules'; import FooComponent from './FooComponent'; export default function FooComponentContainer(props) { // hooks // NOTE: `useRedux` does not exist yet and probably won't look like this const { state, dispatch } = useRedux(); // dispatch action onMount useEffect(() => { dispatch(fetchSomething_reduxAction); }, []); // the empty array tells react to only fire on mount // //reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects // mapping const props = getValues(state, match); // natural component return ; } FooComponentContainer.propTypes = { /* ... */ }; 

Firing events on prop changes:

useEffect has the ability to watch your property between re-renders and calls the function you give it when the property changes.

Before useEffect I found myself adding unnatural lifecycle methods and onPropertyChanged props because I didn’t have a way to do the property diffing outside the component:

import React from 'react'; import PropTypes from 'prop-types'; /** * Before `useEffect`, I found myself adding "unnatural" props * to my components that only fired events when the props diffed. * * I'd find that the component's `render` didn't even use `id` * most of the time */ export default class BeforeUseEffect extends React.Component { static propTypes = { id: PropTypes.string.isRequired, onIdChange: PropTypes.func.isRequired, }; componentDidMount() { this.props.onIdChange(this.props.id); } componentDidUpdate(prevProps) { if (prevProps.id !== this.props.id) { this.props.onIdChange(this.props.id); } } render() { return // ... } }

Now with useEffect there is a very lightweight way to fire on prop changes and our actual component doesn’t have to add props that are unnecessary to its function.

import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { useRedux } from 'react-redux'; import fetchSomething_reduxAction from 'your-redux-stuff'; import getValues from './helpers/getVaules'; import FooComponent from './FooComponent'; export default function FooComponentContainer({ id }) { // hooks // NOTE: `useRedux` does not exist yet and probably won't look like this const { state, dispatch } = useRedux(); // dispatch action onMount useEffect(() => { dispatch(fetchSomething_reduxAction); }, [id]); // `useEffect` will watch this `id` prop and fire the effect when it differs // //reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects // mapping const props = getValues(state, match); // natural component return ; } FooComponentContainer.propTypes = { id: PropTypes.string.isRequired, }; 
Disclaimer: before useEffect there were ways of doing prop diffing inside a container using other higher order components (like recompose’s lifecycle) or creating a lifecycle component like react router does internally, but these ways were either confusing to the team or were unconventional.

What are the benefits here?

Components stay fun

For me, creating components is the most fun and satisfying part of front-end development. You get to turn your team’s ideas and dreams into real experiences and that’s a good feeling I think we all relate to and share.

There will never be a scenario where your component’s API and experience is ruined by the “outside world”. Your component gets to be what you imagined it without extra props — that’s my favorite benefit of this golden rule.

More opportunities to test and reuse

When you adopt an architecture like this, you’re essentially bringing a new data-y layer to the surface. In this “layer” you can switch gears where you’re more concerned about the correctness of data going into your component vs. how your component works.

Whether you’re aware of it or not, this layer already exists in your app but it may be coupled with presentational logic. What I’ve found is that when I surface this layer, I can make a lot of code optimizations and reuse a lot of logic that I would’ve otherwise rewritten without knowing the commonalities.

I think this will become even more obvious with the addition of custom hooks. Custom hooks gives us a much simpler way to extract logic and subscribe to external changes — something that a helper function could not do.

Maximize team throughput

When working on a team, you can separate the development of containers and components. If you agree on APIs beforehand, you can concurrently work on:

  1. Web API (i.e. back-end)
  2. Fetching data from the web API (or similar) and transforming the data to the component’s APIs
  3. The components

Are there any exceptions?

Much like the real Golden Rule, this golden rule is also a golden rule of thumb. There are some scenarios where it makes sense to write a seemingly unnatural component API to reduce the complexity of some transformations.

A simple example would the names of props. It would make things more complicated if engineers renamed data keys under the argument that it’s more “natural”.

It’s definitely possible to take this idea too far where you end up overgeneralizing too soon, and that can also be a trap.

The bottom line

Мање више, ово „златно правило“ једноставно преусмерава постојећу идеју презентационих компонената у односу на компоненте контејнера у новом светлу. Ако на основном нивоу процените шта је потребно вашој компоненти, вероватно ћете добити једноставније и читљивије делове.

Хвала вам!