Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Context provider can't be found #3044

Closed
Bauke opened this issue Feb 25, 2021 · 2 comments
Closed

Context provider can't be found #3044

Bauke opened this issue Feb 25, 2021 · 2 comments
Labels

Comments

@Bauke
Copy link

Bauke commented Feb 25, 2021

Reproduction

I haven't been able to create a contained reproducible example but it does happen to other people on different browsers. This used to work when I first wrote it but at some point an update somewhere must have broken it. I've tried looking at it and through the Preact documentation but as I'm pretty inexperienced with Preact I can't figure out what I'm doing wrong.

You can either build the project from source (yarn watch in one terminal and yarn start:firefox/yarn start:chromium in another) or install the extension from Mozilla Addons.

Steps to reproduce

In the extension page (click on the extension icon to go there) enabling or disabling a feature in the setting header does not work.

Expected Behavior

The feature should get toggled.

Actual Behavior

Preact errors out saying it can't find the provider.

The error message
Uncaught TypeError: u is undefined
    F index.js:239
    toggleFeature index.ts:14
    Header index.ts:25
    $ props.js:150
    C props.js:98
    A props.js:31
    z index.js:395
    T index.js:229
    b children.js:133
    z index.js:402
    T index.js:229
    b children.js:133
    T index.js:195
    b children.js:133
    z index.js:402
    T index.js:229
    b children.js:133
    T index.js:195
    b children.js:133
    T index.js:195
    b children.js:133
    z index.js:402
    T index.js:229
    b children.js:133
    z index.js:402
    T index.js:229
    b children.js:133
    T index.js:195
    b children.js:133
    T index.js:195
    b children.js:133
    T index.js:195
    N render.js:38
    parcelRequire<["settings-page.ts"]</</< settings-page.ts:21
    fulfilled settings-page.fe31a051.js:6134
    promise callback*step settings-page.fe31a051.js:6149
    __awaiter settings-page.fe31a051.js:6152
    __awaiter settings-page.fe31a051.js:6131
    parcelRequire<["settings-page.ts"]</< settings-page.ts:18
    EventListener.handleEvent*parcelRequire<["settings-page.ts"]< settings-page.ts:18
    newRequire settings-page.fe31a051.js:47
    parcelRequire settings-page.fe31a051.js:81
    <anonymous> settings-page.fe31a051.js:120
index.js:239:18
    F index.js:239
    toggleFeature index.ts:14
    Header index.ts:25
    $ props.js:150
    (Async: EventListener.handleEvent)
    C props.js:98
    A props.js:31
    z index.js:395
    T index.js:229
    b children.js:133
    z index.js:402
    T index.js:229
    b children.js:133
    T index.js:195
    b children.js:133
    z index.js:402
    T index.js:229
    b children.js:133
    T index.js:195
    b children.js:133
    T index.js:195
    b children.js:133
    z index.js:402
    T index.js:229
    b children.js:133
    z index.js:402
    T index.js:229
    b children.js:133
    T index.js:195
    b children.js:133
    T index.js:195
    b children.js:133
    T index.js:195
    N render.js:38
    parcelRequire<["settings-page.ts"]</</< settings-page.ts:21
    next self-hosted:1431
    fulfilled settings-page.fe31a051.js:6134
    (Async: promise callback)
    step settings-page.fe31a051.js:6149
    __awaiter settings-page.fe31a051.js:6152
    __awaiter settings-page.fe31a051.js:6131
    parcelRequire<["settings-page.ts"]</< settings-page.ts:18
    (Async: EventListener.handleEvent)
    parcelRequire<["settings-page.ts"]< settings-page.ts:18
    newRequire settings-page.fe31a051.js:47
    parcelRequire settings-page.fe31a051.js:81
    <anonymous> settings-page.fe31a051.js:120

Explanation of my code

Creating the context

Link to code.

type AppContextValues = {
  settings: Settings;
  setActiveFeature: (feature: string) => void;
  toggleFeature: (feature: string) => void;
};

// We create this context with null as we'll create the state and the other
// functions inside App itself. See `settings-page.ts` for that.
export const AppContext = createContext<AppContextValues>(null!);
Creating the state

Link to code.

  // Create some state to set the enabled features.
  const [enabledFeatures, _setFeature] = useState(
    new Set(
      Object.entries(settings.features)
        .filter(([_, value]) => value)
        .map(([key, _]) => key)
    )
  );
  function toggleFeature(feature: string) {
    settings.features[feature] = !settings.features[feature];
    const features = new Set(
      Object.entries(settings.features)
        .filter(([_, value]) => value)
        .map(([key, _]) => key)
    );
    _setFeature(features);
    void setSettings(settings);
  }
Creating the context provider

Link to code.

  return html`
  <${AppContext.Provider} value=${{
    settings,
    setActiveFeature,
    toggleFeature
  }}>
  <!-- All the components go here -->
  </${AppContext.Provider}>`
The component that fails

Link to code.

function toggleFeature(feature: string) {
  const {toggleFeature: toggle} = useContext(AppContext);
  toggle(feature);
}

function Header(props: SettingProps): TRXComponent {
  const enabled = props.enabled ? 'Enabled' : 'Disabled';

  return html`<header>
    <h2>${props.title}</h2>
    <button
      onClick="${() => {
        toggleFeature(props.feature);
      }}"
    >
      ${enabled}
    </button>
  </header>`;
}
Creating the component

Link to code.

// A base component for all the settings, this adds the header and the
// enable/disable buttons. This can also be used as a placeholder for new
// settings when you're still developing them.
export function Setting(props: SettingProps): TRXComponent {
  const children =
    props.children === undefined
      ? html`<p class="info">This setting still needs a component!</p>`
      : props.children;

  const enabled = (props.enabled ? 'Enabled' : 'Disabled').toLowerCase();

  return html`
    <section class="setting ${props.class} ${enabled}">
      <${Header} ...${props} />
      <div class="content">${children}</div>
    </section>
  `;
}
@marvinhagemeister
Copy link
Member

marvinhagemeister commented Feb 25, 2021

The way Hooks work comes with one strong caveat by design, and that is that they must called consistently when a component renders. What's happening here is that this contract is broken in the Header component. The following is a simplified version:

function toggleFeature(feature) {
   const ctx = useContext(AppContext);
   ctx.toggleFeature(feature);
}

function Header(props) {
  return html`<header>
    <button
      onClick="${() => {
        toggleFeature(props.feature);
      }}"
    >
      enable/disable
    </button>
  </header>`;
}

Notice that the useContext hook is not part of the component's render cycle, but called inside an event listener. If we inline the toggleFeature function we end up with the following code:

function Header(props) {
  return html`<header>
    <button
      onClick="${() => {
        // BAD: Hook is not part of the component's render phase
        const ctx = useContext(AppContext);
        ctx.toggleFeature(props.feature);
      }}"
    >
      enable/disable
    </button>
  </header>`;
}

To fix our broken contract we need to ensure that the useContext hook is called during the component's render phase.

function Header(props) {
  // GOOD: Hooks should be called here
  const ctx = useContext(AppContext);

  return html`<header>
    <button
      onClick="${() => ctx.toggleFeature(props.feature)}"
    >
      enable/disable
    </button>
  </header>`;
}

And voilà, changing settings work as expected 👍

@Bauke
Copy link
Author

Bauke commented Feb 25, 2021

Oh my god, thank you so much! 💖 I knew it was going to be just a simple answer of "yeah you're doing it wrong". This makes complete sense (and I even did it this way in another component too, silly me). Thanks for the quick response!

Bauke added a commit to tildes-community/tildes-reextended that referenced this issue Jun 18, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants