Sådan skriver du en React-komponent uden brug af klasser eller kroge

Med udgivelsen af ​​React Hooks har jeg set mange stillinger, der sammenligner klassekomponenter med funktionelle komponenter. Funktionelle komponenter er ikke noget nyt i React, men det var ikke muligt før version 16.8.0 at oprette en stateful komponent med adgang til livscyklus kroge ved kun at bruge en funktion. Eller var det?

Kald mig en pedant (mange mennesker gør det allerede!), Men når vi taler om klassekomponenter, taler vi teknisk om komponenter oprettet af funktioner. I dette indlæg vil jeg gerne bruge React til at demonstrere, hvad der faktisk sker, når vi skriver en klasse i JavaScript.

Klasser vs funktioner

For det første vil jeg meget kort vise, hvordan det, der almindeligvis omtales som funktionelle og klassekomponenter, relaterer til hinanden. Her er en simpel komponent skrevet som en klasse:

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

Hello!

} }

Og her er det skrevet som en funktion:

function Hello() { return 

Hello!

}

Bemærk, at den funktionelle komponent kun er en gengivelsesmetode. På grund af dette var disse komponenter aldrig i stand til at holde deres egen tilstand eller udføre bivirkninger på punkter i løbet af deres livscyklus. Siden React 16.8.0 har det været muligt at oprette stateful funktionelle komponenter takket være kroge, hvilket betyder at vi kan dreje en komponent som denne:

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; } }

I en funktionel komponent som denne:

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; }

Formålet med denne artikel er ikke at komme ind i argumenter om, at den ene er bedre end den anden, da der allerede er hundredvis af indlæg om dette emne! Årsagen til at vise de to komponenter ovenfor er, så vi kan være klare over, hvad React rent faktisk gør med dem.

I tilfælde af klassekomponenten opretter React en forekomst af klassen ved hjælp af newnøgleordet:

const instance = new Component(props); 

Denne forekomst er et objekt. Når vi siger, at en komponent er en klasse, hvad vi egentlig mener er, at det er et objekt. Denne nye objektkomponent kan have sin egen tilstand og metoder, hvoraf nogle kan være livscyklusmetoder (gengivelse, componentDidMount osv.), Som React kalder på de relevante punkter i appens levetid.

Med en funktionel komponent kalder React det bare som en almindelig funktion (fordi det er en almindelig funktion!), Og den returnerer enten HTML eller flere React-komponenter.

Metoder til at håndtere komponenttilstand og udløse effekter på punkter i komponentens livscyklus skal nu importeres, hvis de er nødvendige. Disse fungerer udelukkende baseret på rækkefølgen, i hvilken de kaldes af hver komponent, der bruger dem, da de ikke ved, hvilken komponent der har kaldt dem. Derfor kan du kun kalde kroge på komponentens øverste niveau, og de kan ikke kaldes betinget.

Konstruktorfunktionen

JavaScript har ikke klasser. Jeg ved, det ser ud til, at det har klasser, vi har lige skrevet to! Men under emhætten er JavaScript ikke et klassebaseret sprog, det er prototype-baseret. Klasser blev tilføjet med ECMAScript 2015-specifikationen (også kaldet ES6) og er bare en renere syntaks for eksisterende funktionalitet.

Lad os prøve at omskrive en React-klassekomponent uden at bruge klassesyntaks. Her er den komponent, som vi vil genskabe:

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}

); } }

Dette gengiver en knap, der øger en tæller, når du klikker på den, den er en klassiker! Den første ting, vi skal oprette, er konstruktorfunktionen, dette udfører de samme handlinger, som constructormetoden i vores klasse udfører bortset fra opkaldet til, superfordi det er en eneste ting.

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

Dette er den funktion, som React kalder med newnøgleordet. Når en funktion kaldes med, newbehandles den som en konstruktorfunktion; et nyt objekt oprettes, thisvariablen peges på det, og funktionen udføres med det nye objekt, der bruges, uanset hvor det thiser nævnt.

Next, we need to find a home for the render and handleClick methods and for that we need to talk about the prototype chain.

The Prototype Chain

JavaScript allows inheritance of properties and methods between objects through something known as the prototype chain.

Well, I say inheritence, but I actually mean delegation. Unlike in other languages with classes, where properties are copied from a class to its instances, JavaScript objects have an internal protoype link which points to another object. When you call a method or attempt to access a property on an object, JavaScript first checks for the property on the object itself. If it can't find it there then it checks the object's prototype (the link to the other object). If it still can't find it, then it checks the prototype's prototype and so on up the chain until it either finds it or runs out of prototypes to check.

Generally speaking, all objects in JavaScript have Object at the top of their prototype chain; this is how you have access to methods such as toString and hasOwnProperty on all objects. The chain ends when an object is reached with null as its prototype, this is normally at Object.

Let's try to make things clearer with an example.

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

First we create parentObject. Because we've used the object literal syntax this object will be linked to Object. Next we use Object.create to create a new object using parentObject as its prototype.

Now, when we use console.log to print our childObject we should see:

konsol output af 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:

udvidet output af 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.

Jeg håber, du har fundet denne artikel interessant og / eller behagelig. Du kan finde flere indlæg, som jeg har skrevet på min blog på hellocode.dev. Tak skal du have.