Dansekongen
Dansekongen

Reputation: 338

Why does not a context provider at the top get called before sub components using it?

In the following code the console log appears in the following order: "Card Body", undefined "CardProvider", {lighter: true}

This means that it render component before calling the provider, even if the provider is at the top level of the components.

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

interface CardProps {
    children: JSXElement;
    lighter: boolean;
}

const CardContext = createContext<{ lighter: boolean }>();

function CardProvider(props: CardProps) {
    const [lighter] = createSignal(props.lighter);
    const value = { lighter: lighter() };
    console.log("CardProvider", value);
    return (
        <CardContext.Provider value={value}>
            {props.children}
        </CardContext.Provider>
    );
}

function useInternalState() {
    return useContext(CardContext);
}

export function Card({ children, lighter = false }: CardProps) {
    return (
        <CardProvider lighter={lighter}>
            <div
                class={`transition-all ease-in-out duration-300 bg-gray-800 border-[1px] border-gray-100 border-opacity-10 p-2 rounded-md my-2 md:basis-1/3 text-center md:mr-2 flex items-center hover:bg-gray-900 ${
                    lighter ? "bg-gray-200 hover:bg-gray-300" : ""
                }`}
            >
                {children}
            </div>
        </CardProvider>
    );
}

interface CardTitleProps {
    title: string;
}

Card.Title = ({ title }: CardTitleProps) => {
    const state = useInternalState();
    return (
        <div
            class={`${
                state?.lighter ? "text-gray-900" : "text-gray-100"
            } text-lg font-semibold`}
        >
            {title}
        </div>
    );
};

interface CardBodyProps {
    body: string;
}

Card.Body = ({ body }: CardBodyProps) => {
    const state = useInternalState();
    console.log("Card Body", state);
    return (
        <div
            class={`${
                state?.lighter ? "text-gray-600" : "text-gray-400"
            } text-md font-semibold`}
        >
            {body}
        </div>
    );
};

The use of the code is as follows

<Card lighter={false}>
  <Card.Title title="Title" />
  <Card.Body body="Body" />
</Card>

Why does this happen and is it a fix for this(e.g. something I'm doing wrong?)

Upvotes: 0

Views: 283

Answers (1)

snnsnn
snnsnn

Reputation: 13698

Context API run synchronously, so value is always available when the component runs but your problem is how Solid runs components.

Solid runs props.children first, so that it can be passed to its parent component. Since props.children of the Card component runs before the Card component, children will receive the default value, which is `undefined.

To workaround this solid uses a getter function to pass childrens value:

const Card = (props) => {
  return (
    <Context.Provider value={'blue'}>
      {props.children}
    </Context.Provider>
  );
}

The code above will produce the following code when compiled:

const Card = props => {
  return _$createComponent(Context.Provider, {
    value: 'blue',
    get children() {
      return props.children;
    }
  });
};

But if you use destructure the props, compiler will not produce the getter function:

const Card = ({ children }) => {
  return (
    <Context.Provider value={'blue'}>
      {children}
    </Context.Provider>
  );
}

Here is the output code for the desctructured value:

const Card = ({
  children
}) => {
  return _$createComponent(Context.Provider, {
    value: 'blue',
    children: children
  });
};

If you do not use prop destructuring everything work as expected. But if you do, you need have extra measures as explained below.

You should move your provider somewhere higher on the component tree, so that when Card's children is called, they should receive the context value. In other words, you should wrap your component with the provider.

const App = () => {
  return (
    <CardProvider>
      <Card>
        <Card.Title title="Title" />
        <Card.Body body="Body" />
      </Card>
    </CardProvider>
  );
};

Wrapping Card's children with the provider also works:

const App = () => {
  return (
    <Card>
      <CardProvider>
        <Card.Title title="Title" />
        <Card.Body body="Body" />
      </CardProvider>
    </Card>
  );
};

Here is a working demo, however you should know that we use context API so that we don't pass values through props, it is a shortcut in a way in which downstream components get their data from the provider, but you use props unnecessarily. Also, you should not destructure your props because you will remove reactivity.

import { JSXElement, createContext, createSignal, useContext } from 'solid-js';
import { render } from 'solid-js/web';

interface CardProps {
  children: JSXElement;
}

const CardContext = createContext<{ lighter: boolean }>();

function CardProvider(props: { children: JSXElement }) {
  const [lighter] = createSignal(false);
  const value = { lighter: lighter() };
  console.log('CardProvider', value);
  return (
    <CardContext.Provider value={value}>{props.children}</CardContext.Provider>
  );
}

export function Card(props: CardProps) {
  return <div>{props.children}</div>;
}

interface CardTitleProps {
  title: string;
}

Card.Title = (props: CardTitleProps) => {
  const state = useContext(CardContext);
  return (
    <div
      class={`${
        state?.lighter ? 'text-gray-900' : 'text-gray-100'
      } text-lg font-semibold`}
    >
      {props.title}
    </div>
  );
};

interface CardBodyProps {
  body: string;
}

Card.Body = (props: CardBodyProps) => {
  const state = useContext(CardContext);
  console.log('Card Body', state);
  return (
    <div
      class={`${
        state?.lighter ? 'text-gray-600' : 'text-gray-400'
      } text-md font-semibold`}
    >
      {props.body}
    </div>
  );
};

export const App = () => {
  return (
    <CardProvider>
      <Card>
        <Card.Title title="Title" />
        <Card.Body body="Body" />
      </Card>
    </CardProvider>
  );
};

render(App, document.body);

Upvotes: 1

Related Questions