Putting Things in Context With React

Avatar of Neal Fennimore
Neal Fennimore on

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Context ist derzeit eine experimentelle API für React – aber bald ein vollwertiges Element! Es gibt viele Gründe, warum es interessant ist, aber vielleicht am wichtigsten ist, dass es übergeordneten Komponenten ermöglicht, Daten implizit an ihre Kindkomponenten weiterzugeben, unabhängig davon, wie tief der Komponentenbaum ist. Mit anderen Worten, Daten können einer übergeordneten Komponente hinzugefügt werden, und dann kann jede Kindkomponente darauf zugreifen.

See the Pen React Context Lights by Neal Fennimore (@nealfennimore) on CodePen.

Dies ist zwar oft der Anwendungsfall für die Verwendung von etwas wie Redux, aber es ist gut, wenn Sie keine komplexe Datenverwaltung benötigen. Denken Sie darüber nach! Wir erstellen einen benutzerdefinierten Datenstrom und entscheiden, welche Props auf welchen Ebenen weitergegeben werden. Ziemlich cool.

Context ist großartig für Bereiche, in denen viele Komponenten von einer einzelnen Datenquelle abhängen, sich aber tief im Komponentenbaum befinden. Das explizite Weitergeben jeder Prop an jede einzelne Komponente kann oft überwältigend sein, und es ist hier einfach viel einfacher, Context zu verwenden.

Betrachten wir zum Beispiel, wie wir Props normalerweise den Baum hinunter weitergeben würden. In diesem Fall übergeben wir die Farbe red mit Props an jede Komponente, um sie weiter im Stream zu bewegen.

class Parent extends React.Component {
  render(){
    return <Child color="red" />;
  }
}

class Child extends React.Component {
  render(){
    return <GrandChild color={this.props.color} />
  }
}

class GrandChild extends React.Component {
  render(){
    return (
      <div style={{color: this.props.color}}>
        Yep, I'm the GrandChild
      </div>
    );
  }
}

Was wäre, wenn wir niemals wollten, dass die Child-Komponente die Prop überhaupt hat? Context erspart uns, dass wir die Child-Komponente mit der Farbe durchlaufen müssen und sie direkt von der Parent an die GrandChild weitergeben.

class Parent extends React.Component {
  // Allow children to use context
  getChildContext() {
    return {
      color: 'red'
    };
  }
  
  render(){
    return <Child />;
  }
}

Parent.childContextTypes = {
  color: PropTypes.string
};

class Child extends React.Component {
  render() {
    // Props is removed and context flows through to GrandChild
    return <GrandChild />
  }
}

class GrandChild extends React.Component {
  render() {
    return (
      <div style={{color: this.context.color}}>
        Yep, I'm still the GrandChild
      </div>
    );
  }
}

// Expose color to the GrandChild
GrandChild.contextTypes = {
  color: PropTypes.string
};

Obwohl etwas ausführlicher, ist der Vorteil, die color irgendwo im Komponentenbaum verfügbar zu machen. Nun, manchmal…

Es gibt einige Haken

Man kann nicht alles haben und das Glück genießen, und Context in seiner aktuellen Form bildet da keine Ausnahme. Es gibt einige zugrunde liegende Probleme, auf die Sie höchstwahrscheinlich stoßen werden, wenn Sie Context für alles außer den einfachsten Fällen verwenden.

Context ist großartig für die Verwendung bei einem initialen Rendern. Context unterwegs aktualisieren? Nicht so sehr. Ein häufiges Problem mit Context ist, dass Änderungen an Context **nicht** immer in einer Komponente reflektiert werden.

Lassen Sie uns diese Haken genauer untersuchen.

Haken 1: Verwendung von Pure Components

Context ist schwierig bei der Verwendung von PureComponent, da standardmäßig keine flache Differenzierung mit Context durchgeführt wird. Flache Differenzierung mit PureComponent prüft, ob die Werte des Objekts strikt gleich sind. Wenn nicht, dann (und nur dann) wird die Komponente aktualisiert. Da aber Context nicht geprüft wird, passiert nichts.

See the Pen React Context Lights with PureComponents by Neal Fennimore (@nealfennimore) on CodePen.

Haken 2: Should Component Update? Vielleicht.

Context wird auch nicht aktualisiert, wenn die shouldComponentUpdate einer Komponente false zurückgibt. Wenn Sie eine benutzerdefinierte shouldComponentUpdate-Methode haben, müssen Sie auch Context berücksichtigen. Um Updates mit Context zu ermöglichen, könnten wir jede einzelne Komponente mit einer benutzerdefinierten shouldComponentUpdate aktualisieren, die ungefähr so aussieht.

import shallowEqual from 'fbjs/lib/shallowEqual';

class ComponentThatNeedsColorContext extends React.PureComponent {
  // nextContext will show color as soon as we apply ComponentThatNeedsColorContext.contextTypes
  // NOTE: Doing the below will show a console error come react v16.1.1
  shouldComponentUpdate(nextProps, nextState, nextContext){
    return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState) || !shallowEqual(this.context, nextContext);
  }
}

ComponentThatNeedsColorContext.contextTypes = {
  color: PropTypes.string
};

Dies löst jedoch nicht das Problem, dass eine zwischengeschaltete PureComponent zwischen der übergeordneten und der untergeordneten Komponente Context-Updates blockiert. Das bedeutet, dass jede PureComponent zwischen der übergeordneten und der untergeordneten Komponente contextTypes darauf definiert haben müsste, und sie müssten auch eine aktualisierte shouldComponentUpdate-Methode haben. Und zu diesem Zeitpunkt ist das viel Arbeit für sehr wenig Gewinn.

Bessere Ansätze für die Haken

Glücklicherweise haben wir einige Möglichkeiten, die Haken zu umgehen.

Ansatz 1: Verwenden Sie eine Higher Order Component

Eine Higher Order Component kann aus dem Context lesen und die benötigten Werte als Prop an die nächste Komponente weitergeben.

import React from 'react';

const withColor = (WrappedComponent) => {
    class ColorHOC extends React.Component {
        render() {
            const { color } = this.context;        
            return <WrappedComponent style={{color: color}} {...this.props} />
        }
    }
         
    ColorHOC.contextTypes = {
        color: React.PropTypes.string  
    };

    return ColorHOC;
};


export const Button = (props)=> <button {...props}>Button</button>

// ColoredButton will render with whatever color is currently in context with a style prop
export const ColoredButton = withColor( Button );

See the Pen React Context Lights with HOC by Neal Fennimore (@nealfennimore) on CodePen.

Ansatz 2: Verwenden Sie Render Props

Render Props ermöglichen es uns, Props zu verwenden, um Code zwischen zwei Komponenten zu teilen.

class App extends React.Component {
    getChildContext() {
        return {
            color: 'red'
        }
    }

    render() {
        return <Button />
    }
}

App.childContextTypes = {
    color: React.PropTypes.string
}

// Hook 'Color' into 'App' context
class Color extends React.Component {
    render() {
        return this.props.render(this.context.color);
    }
}

Color.contextTypes = {
    color: React.PropTypes.string
}

class Button extends React.Component {
    render() {
        return (
            <button type="button">
                {/* Return colored text within Button */}
                <Color render={ color => (
                    <Text color={color} text="Button Text" />
                ) } />
            </button>
        )
    }
}

class Text extends React.Component {
    render(){
        return (
            <span style={{color: this.props.color}}>
                {this.props.text}
            </span>
        )
    }
}

Text.propTypes = {
    text: React.PropTypes.string,
    color: React.PropTypes.string,
}

Ansatz 3: Dependency Injection

Eine dritte Möglichkeit, diese Haken zu umgehen, ist die Verwendung von Dependency Injection, um die Context-API einzuschränken und Komponenten zu ermöglichen, sich nach Bedarf zu abonnieren.

Der neue Context

Die neue Art der Verwendung von Context, die derzeit für die nächste kleine Veröffentlichung von React (16.3) geplant ist, bietet die Vorteile, lesbarer und einfacher zu schreiben zu sein, ohne die "Haken" früherer Versionen. Wir haben jetzt eine neue Methode namens createContext, die einen neuen Context definiert und sowohl einen Provider als auch einen Consumer zurückgibt.

Der Provider stellt einen Context her, in den sich alle untergeordneten Komponenten einklinken können. Er wird über den Consumer eingehängt, der eine Render-Prop verwendet. Das erste Argument dieser Render-Prop-Funktion ist der value, den wir dem Provider gegeben haben. Durch die Aktualisierung des Werts innerhalb des Provider werden alle Consumer aktualisiert, um den neuen Wert widerzuspiegeln.

Als Nebeneffekt bei der Verwendung des neuen Context müssen wir nicht mehr childContextTypes, getChildContext und contextTypes verwenden.

const ColorContext = React.createContext('color');
class ColorProvider extends React.Component {
    render(){
        return (
            <ColorContext.Provider value={'red'}>
                { this.props.children }
            </ColorContext.Provider>
        )
    }
}

class Parent extends React.Component {  
    render(){
        // Wrap 'Child' with our color provider
        return (
            <ColorProvider>
                <Child />
            </ColorProvider>
        );
    }
}

class Child extends React.Component {
    render(){
        return <GrandChild />
    }
}

class GrandChild extends React.Component {
    render(){
        // Consume our context and pass the color into the style attribute
        return (
            <ColorContext.Consumer>
                {/* 'color' is the value from our Provider */}
                {
                    color => (
                        <div style={{color: color}}>
                            Yep, I'm still the GrandChild
                        </div>
                    )
                }
            </ColorContext.Consumer>
        );
    }
}

Separate Kontexte

Da wir eine granularere Kontrolle darüber haben, wie wir Context exponieren und welche Komponenten ihn verwenden dürfen, können wir Komponenten individuell mit unterschiedlichen Kontexten umschließen, auch wenn sie sich innerhalb derselben Komponente befinden. Dies sehen wir im nächsten Beispiel, bei dem wir durch die doppelte Verwendung von LightProvider zwei Komponenten einen separaten Context geben können.

See the Pen React Context Lights with new Context by Neal Fennimore (@nealfennimore) on CodePen.

Fazit

Context ist eine mächtige API, aber es ist auch sehr einfach, sie falsch zu verwenden. Es gibt auch einige Vorbehalte bei der Verwendung, und es kann sehr schwierig sein, Probleme zu identifizieren, wenn Komponenten fehlschlagen. Während Higher-Order Components und Dependency Injection Alternativen für die meisten Fälle bieten, kann Context vorteilhaft in isolierten Teilen Ihrer Codebasis verwendet werden.

Mit dem nächsten Context müssen wir uns jedoch keine Sorgen mehr über die Haken machen, die wir mit der vorherigen Version hatten. Er erspart uns die Definition von contextTypes auf einzelnen Komponenten und eröffnet das Potenzial, neue Kontexte wiederverwendbar zu definieren.