javascriptjsxsolid-js

SolidJS Context Provider Specification


I'm following context example from the tutorial, what I understand from the example is using a customized provider:

import { createSignal, createContext, useContext } from "solid-js";

const CounterContext = createContext();

export function CounterProvider(props) {
  const [count, setCount] = createSignal(props.count || 0),
    counter = [
      count,
      {
        increment() {
          setCount((c) => c + 1);
        },
        decrement() {
          setCount((c) => c - 1);
        },
      },
    ];

  return (
    <CounterContext.Provider value={counter}>
      {props.children}
    </CounterContext.Provider>
  );
}

export function useCounter() {
  return useContext(CounterContext);
}

I have three questions:

  1. I couldn't find any specification about how to define a custom context provider other than example above, is there any standard or specification to follow?

  2. Where in this example is the binding between the CounterContext and the CounterProvider? Is it in this line? <CounterContext.Provider value={counter}>. Combined with the createSignal then used in counter?

So the dependency would be: createSignal->counter->CounterProvider?

  1. I couldn't find any context example in jsx format about createContext with more complex objects, only in typescript syntax. Could this be a valid example?
const SomeContext = createContext({
  someProp: "defaultString",
  someAction: function(){
    console.log('something')
  }
});

Solution

  • Context is a object on the owner of the current reactive scope. It keeps the key-value pairs set by providers.

    owner.context = { [id]: value };
    

    When a component runs, it creates a reactive scope, hence a new owner. We will elaborate on this later on.

    Context API is made up of three components:

    1. createContext creates a unique id and a Provider component.
    2. Provider components uses the unique id to set a value on the context object.
    3. useContext returns the previously set value using the unique id passed to it. If current owner does not have the id-value pair, useContext checks owner's parent, then its grand parent, so on so forth until all ancestor owners are exhausted. If none of them has it, returns the default value.

    Context is a way to pass values down the component tree without going through the component hierarchy. I used the term passing down, because people tend to explain how context works, but in reality it is the other way around: The useContext hook climbs up the component tree looking for a specific context value using the provided key.

    Here is how Solid's context API works:

    When you create context via createContext, you will be creating a unique id and a Provider component. That is it. You get an id and a component but you won't be setting anything yet.

    Here is the actual code used in Solid:

    function createContext(defaultValue) {
      const id = Symbol("context");
      return { id, Provider: createProvider(id), defaultValue };
    }
    

    https://www.solidjs.com/docs/latest#createcontext

    Provider component will use this id when setting a value, which we will do next:

    <Context.Provider value={someValue}></Context.Provider>
    

    Context.Provider components runs createProvider function internally which in turn sets a value on the context object of the current owner:

    Owner!.context = { [id]: props.value };
    

    Owner.context is an object because you can provide any number of context.

    Owner is the reactive scope that owns the currently running code. Computations (effects, memos, etc.) create owners which are children of their owner, all the way up to the root owner created by createRoot or render. In this sense, components are also effects. When you create a component, you are creating an owner.

    Owners are used to keep track of who creates whom, so that they will be disposed when their owner gets discarded. If you create an effect inside another effect, you will be creating an owner inside another owner. Basically it is a tree.

    You can learn more about owner at:

    https://www.solidjs.com/docs/latest/api#getowner

    Now that we created context and set a value, it is time to access it. We use the useContext hook for that purpose.

    The useContext hooks needs an id in order to look up a context value. When we pass the value that we get from the createContext function, we will be providing that id.

    export function useContext<T>(context: Context<T>): T {
      let ctx;
      return (ctx = lookup(Owner, context.id)) !== undefined ? ctx : context.defaultValue;
    }
    

    If you check the code, you will see it is a walker. The hook looks up its immediate owner for a value. If value is there, it will return it, if not, it will get the owner of the current owner, and look it up. So on and so forth, until there are no more owners to look up. In that case, default value is returned.

    To reiterate, the useContext function is a walker that walks the tree of owners for a particular id on their context property.

    That sums up how Solid's context API works.

    The value you provide via Context API can be of any type. It could be static value or could be a signal like the one in your example.

    The example looks complicated because an object with methods stored in the context. Lets try a simpler one.

    import { createContext, useContext } from 'solid-js';
    import { render } from 'solid-js/web';
    
    const CounterContex = createContext<number>(0);
    
    const Child = () => {
      const count = useContext(CounterContex);
      return (
        <div>{count}</div>
      );
    };
    
    const App = () => {
      return (
        <div>
          <CounterContex.Provider value={10}>
            <Child />
          </CounterContex.Provider>
        </div>
      );
    }
    
    render(App, document.querySelector('#app'));
    

    If you do not provide a value, default value will be used:

    import { createContext, useContext } from "solid-js";
    import { render } from "solid-js/web";
    
    const CounterContex = createContext<number>(0);
    
    const Child = () => {
      const count = useContext(CounterContex);
      return <div>{count}</div>;
    };
    
    const App = () => {
      return (
        <div>
          <Child />
        </div>
      );
    };
    
    render(App, document.querySelector("#app"));
    

    You can overwrite the context value at different levels of the component tree:

    import { createContext, useContext } from "solid-js";
    import { render } from "solid-js/web";
    
    const CounterContex = createContext<number>(0);
    
    const Child = () => {
      const count = useContext(CounterContex);
      return <div>{count}</div>;
    };
    
    const App = () => {
      return (
        <div>
          <CounterContex.Provider value={10}>
            <Child />
            <CounterContex.Provider value={20}>
              <Child />
            </CounterContex.Provider>
          </CounterContex.Provider>
        </div>
      );
    };
    
    render(App, document.querySelector("#app"));
    

    Now, lets store a signal on the context and use in inside a child component:

    import { createContext, useContext, createSignal } from "solid-js";
    import { render } from "solid-js/web";
    
    const [count, setCount] = createSignal(0);
    
    const CounterContex = createContext({
      count,
      setCount,
    });
    
    const Child = () => {
      const { count, setCount } = useContext(CounterContex);
    
      return (
        <div onClick={() => setCount(count() + 1)}>
          Click to increment: {count()}
        </div>
      );
    };
    
    const App = () => {
      return (
        <div>
          <Child />
        </div>
      );
    };
    
    render(App, document.querySelector("#app"));
    

    Lets refactor the previous example. In this one, we will use undefined as the default value but overwrite it later with a getter and setter from a signal using a context provider:

    import { createContext, useContext, createSignal } from "solid-js";
    import { render } from "solid-js/web";
    
    const CounterContex = createContext<any>();
    
    const Child = () => {
      const { count, setCount } = useContext(CounterContex);
      return (
        <div onClick={() => setCount(count() + 1)}>Click to increment: {count}</div>
      );
    };
    
    const [count, setCount] = createSignal(0);
    const App = () => {
      return (
        <div>
          <CounterContex.Provider value={{ count, setCount }}>
            <Child />
          </CounterContex.Provider>
        </div>
      );
    };
    
    render(App, document.querySelector("#app"));
    

    Now it is time to implement the example you post. Yours is wrapped in a component called CounterProvider but I will post it plainly. You can move the logic into a component any time:

    import { createContext, useContext, createSignal } from "solid-js";
    import { render } from "solid-js/web";
    
    const CounterContex = createContext<any>();
    
    const Child = () => {
      const [count, { increment, decrement }] = useContext(CounterContex);
      return (
        <div>
          <div>{count()}</div>
          <div onClick={() => increment()}>Click to Increment</div>
          <div onClick={() => decrement()}>Click to Decrement</div>
        </div>
      );
    };
    
    const [count, setCount] = createSignal(0);
    
    const o = [
      count,
      {
        increment() {
          setCount((c) => c + 1);
        },
        decrement() {
          setCount((c) => c - 1);
        },
      },
    ];
    
    const App = () => {
      return (
        <div>
          {/* This time we use an array rather than an object as the context value */}
          <CounterContex.Provider value={o}>
            <Child />
          </CounterContex.Provider>
        </div>
      );
    };
    
    render(App, document.querySelector("#app"));