Как да напиша React компонент, без да използвам класове или куки

С пускането на React Hooks видях много публикации, сравняващи компонентите на класа с функционалните компоненти. Функционалните компоненти не са нищо ново в React, но преди версия 16.8.0 не беше възможно да се създаде компонент с състояние с достъп до куки на жизнения цикъл, използвайки само функция. Или беше?

Наречете ме педант (много хора вече го правят!), Но когато говорим за компоненти на класа, ние технически говорим за компоненти, създадени от функции. В тази публикация бих искал да използвам React, за да покажа какво всъщност се случва, когато пишем клас в JavaScript.

Класове срещу функции

Първо, бих искал съвсем накратко да покажа как това, което обикновено се наричат ​​функционални и клас компоненти, се свързват помежду си. Ето един прост компонент, написан като клас:

class Hello extends React.Component { render() { return 

Hello!

} }

И тук е написано като функция:

function Hello() { return 

Hello!

}

Забележете, че функционалният компонент е просто метод за рендиране. Поради това тези компоненти никога не са били в състояние да поддържат собственото си състояние или да извършват някакви странични ефекти в точки през жизнения си цикъл. От React 16.8.0 е възможно да се създават функционални компоненти с състояние благодарение на куки, което означава, че можем да превърнем компонент като този:

class Hello extends React.Component { state = { sayHello: false } componentDidMount = () => { fetch('greet') .then(response => response.json()) .then(data => this.setState({ sayHello: data.sayHello }); } render = () => { const { sayHello } = this.state; const { name } = this.props; return sayHello ? 

{`Hello ${name}!`}

: null; } }

Във функционален компонент като този:

function Hello({ name }) { const [sayHello, setSayHello] = useState(false); useEffect(() => { fetch('greet') .then(response => response.json()) .then(data => setSayHello(data.sayHello)); }, []); return sayHello ? 

{`Hello ${name}!`}

: null; }

Целта на тази статия не е да спорим, че единият е по-добър от другия, тъй като вече има стотици публикации по тази тема! Причината за показване на двата компонента по-горе е, за да можем да сме наясно какво всъщност прави React с тях.

В случая с компонента на класа React създава екземпляр на класа, използвайки newключовата дума:

const instance = new Component(props); 

Този екземпляр е обект. Когато казваме, че компонентът е клас, всъщност имаме предвид, че е обект. Този нов обектен компонент може да има свое собствено състояние и методи, някои от които могат да бъдат методи на жизнения цикъл (render, componentDidMount и др.), Които React ще извиква в подходящите точки по време на живота на приложението.

С функционален компонент React просто го извиква като обикновена функция (защото това е обикновена функция!) И връща HTML или повече React компоненти.

Вече трябва да се импортират методи, с които да се обработват състоянието на компонента и да се задействат ефекти в точки през жизнения цикъл на компонента, ако са необходими. Те работят изцяло въз основа на реда, в който са извикани от всеки компонент, който ги използва, тъй като не знаят кой компонент ги е извикал. Ето защо можете да извиквате куки само на най-горното ниво на компонента и те не могат да бъдат извиквани условно.

Функцията конструктор

JavaScript няма класове. Знам, че изглежда, че има класове, току-що написахме две! Но под капака JavaScript не е език, базиран на клас, той е базиран на прототип. Класовете бяха добавени със спецификацията ECMAScript 2015 (наричана още ES6) и са просто по-чист синтаксис за съществуващата функционалност.

Нека да опитаме да пренапишем компонент на React клас, без да използваме синтаксиса на класа. Ето компонента, който ще пресъздадем:

class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 } this.handleClick = this.handleClick.bind(this); } handleClick() { const { count } = this.state; this.setState({ count: count + 1 }); } render() { const { count } = this.state; return (  +1 

{count}

); } }

Това прави бутон, който увеличава брояч при щракване, това е класика! Първото нещо, което трябва да създадем, е функцията конструктор, която ще изпълнява същите действия, които constructorметодът в нашия клас изпълнява, освен повикването, superзащото това е само клас.

function Counter(props) { this.state = { count: 0 } this.handleClick = this.handleClick.bind(this); } 

Това е функцията, която React ще извика с newключовата дума. Когато дадена функция се извика с newнея, тя се третира като конструктор; създава се нов обект, thisпроменливата се насочва към него и функцията се изпълнява с новия обект, който се използва навсякъде, където thisе споменато.

След това трябва да намерим дом за renderи handleClickметодите и за това трябва да поговорим за веригата прототипи.

Прототипната верига

JavaScript позволява наследяване на свойства и методи между обекти чрез нещо, известно като верига прототип.

Е, казвам наследство, но всъщност имам предвид делегиране. За разлика от други езици с класове, където свойствата се копират от клас в неговите екземпляри, обектите на JavaScript имат вътрешна прототипна връзка, която сочи към друг обект. Когато извикате метод или се опитате да осъществите достъп до свойство на обект, JavaScript първо проверява за свойството на самия обект. Ако не може да го намери там, проверява прототипа на обекта (връзката към другия обект). Ако все още не може да го намери, той проверява прототипа на прототипа и така нагоре по веригата, докато или го намери, или изтече прототипите за проверка.

Най-общо казано, всички обекти в JavaScript са Objectв горната част на веригата си прототипи; по този начин имате достъп до методи като toStringи hasOwnPropertyза всички обекти. Веригата завършва, когато обектът бъде достигнат с nullнегов прототип, това обикновено е в Object.

Нека се опитаме да направим нещата по-ясни с пример.

const parentObject = { name: 'parent' }; const childObject = Object.create(parentObject, { name: { value: 'child' } }); console.log(childObject); 

Първо създаваме parentObject. Тъй като сме използвали синтаксиса на литерала на обекта, този обект ще бъде свързан Object. След това използваме, за Object.createда създадем нов обект, използвайки parentObjectкато негов прототип.

Сега, когато използваме console.logза отпечатване на нашите, childObjectтрябва да видим:

console output of childObject

The object has two properties, there is the name property which we just set and the __proto___ property. __proto__ isn't an actual property like name, it is an accessor property to the internal prototype of the object. We can expand these to see our prototype chain:

expanded output of childObject

The first __proto___ contains the contents of parentObject which has its own __proto___ containing the contents of Object. These are all of the properties and methods that are available to childObject.

It can be quite confusing that the prototypes are found on a property called __proto__! It's important to realise that __proto__ is only a reference to the linked object. If you use Object.create like we have above, the linked object can be anything you choose, if you use the new keyword to call a constructor function then this linking happens automatically to the constructor function's prototype property.

Ok, back to our component. Since React calls our function with the new keyword, we now know that to make the methods available in our component's prototype chain we just need to add them to the prototype property of the constructor function, like this:

Counter.prototype.render = function() { const { count } = this.state; return (  +1 

{count}

); }, Counter.prototype.handleClick = function () { const { count } = this.state; this.setState({ count: count + 1 }); }

Static Methods

This seems like a good time to mention static methods. Sometimes you might want to create a function which performs some action that pertains to the instances you are creating - but it doesn't really make sense for the function to be available on each object's this. When used with classes they are called Static Methods. I'm not sure if they have a name when not used with classes!

We haven't used any static methods in our example, but React does have a few static lifecycle methods and we did use one earlier with Object.create. It's easy to declare a static method on a class, you just need to prefix the method with the static keyword:

class Example { static staticMethod() { console.log('this is a static method'); } } 

And it's equally easy to add one to a constructor function:

function Example() {} Example.staticMethod = function() { console.log('this is a static method'); } 

In both cases you call the function like this:

Example.staticMethod() 

Extending React.Component

Our component is almost ready, there are just two problems left to fix. The first problem is that React needs to be able to work out whether our function is a constructor function or just a regular function. This is because it needs to know whether to call it with the new keyword or not.

Dan Abramov wrote a great blog post about this, but to cut a long story short, React looks for a property on the component called isReactComponent. We could get around this by adding isReactComponent: {} to Counter.prototype (I know, you would expect it to be a boolean but isReactComponent's value is an empty object. You'll have to read his article if you want to know why!) but that would only be cheating the system and it wouldn't solve problem number two.

In the handleClick method we make a call to this.setState. This method is not on our component, it is "inherited" from React.Component along with isReactComponent. If you remember the prototype chain section from earlier, we want our component instance to first inherit the methods on Counter.prototype and then the methods from React.Component. This means that we want to link the properties on React.Component.prototype to Counter.prototype.__proto__.

Fortunately there's a method on Object which can help us with this:

Object.setPrototypeOf(Counter.prototype, React.Component.prototype); 

It Works!

That's everything we need to do to get this component working with React without using the class syntax. Here's the code for the component in one place if you would like to copy it and try it out for yourself:

function Counter(props) { this.state = { count: 0 }; this.handleClick = this.handleClick.bind(this); } Counter.prototype.render = function() { const { count } = this.state; return (  +1 

{count}

); } Counter.prototype.handleClick = function() { const { count } = this.state; this.setState({ count: count + 1 }); } Object.setPrototypeOf(Counter.prototype, React.Component.prototype);

As you can see, it's not as nice to look at as before. In addtion to making JavaScript more accessible to developers who are used to working with traditional class-based languages, the class syntax also makes the code a lot more readable.

I'm not suggesting that you should start writing your React components in this way (in fact, I would actively discourage it!). I only thought it would be an interesting exercise which would provide some insight into how JavaScript inheritence works.

Although you don't need to understand this stuff to write React components, it certainly can't hurt. I expect there will be occassions when you are fixing a tricky bug where understanding how prototypal inheritence works will make all the difference.

I hope you have found this article interesting and/or enjoyable. You can find more posts that I have written on my blog at hellocode.dev. Thank you.