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

Core: Add async loaders #12699

Merged
merged 22 commits into from
Oct 12, 2020
Merged

Core: Add async loaders #12699

merged 22 commits into from
Oct 12, 2020

Conversation

shilman
Copy link
Member

@shilman shilman commented Oct 8, 2020

Issue: #10009

What I did

Asynchronous loaders to provide external data to stories:

// TodoItem.stories.js

import React from 'react';
import fetch from 'node-fetch';
import { TodoItem } from './TodoItem';

export const Primary = (args, { loaded: { todo } }) => <TodoItem {...args} {...todo} />;
Primary.loaders = [
  async () => ({
    todo: (await fetch('https://jsonplaceholder.typicode.com/todos/1')).json(),
  }),
];

How to test

See updated stories in official-storybook and html-kitchen-sink

@shilman shilman modified the milestones: 6.1 perf, 6.1 async Oct 8, 2020
@shilman shilman added this to the 6.1 perf milestone Oct 12, 2020
@shilman shilman mentioned this pull request Oct 12, 2020
@shilman shilman merged commit dd08c4b into next Oct 12, 2020
@shilman shilman deleted the 1009-async-loaders branch October 12, 2020 11:29
@shilman shilman mentioned this pull request Oct 12, 2020
5 tasks
@agrohs
Copy link

agrohs commented Oct 13, 2020

How would this work if we need to have an async decorator? Something to this regard:

export const getActiveContext = () => {
  return Promise.resolve({ foo: bar }); // NOTE: this would call some long running api to bring back data before resolving
}

const withContextProvider = async (Story, storyContext) => {
  const activeContext = await getActiveContext(storyContext)
  return (
    <StatefulProvider value={activeContext}>
        <Story {...storyContext} />
    </StatefulProvider>
  )
}

export const decorators = [
  withContextProvider,
]

@tmeasday
Copy link
Member

@agrohs: you would just refactor things slightly, and add the loader + decorator together:

export const loaders = [async () => {
  return Promise.resolve({ activeContext: { foo: bar } }),
};

export const decorators = (Story, { loaded: { activeContext } }) => {
  return (
    <StatefulProvider value={activeContext}>
        <Story {...storyContext} />
    </StatefulProvider>
  )
}

Having said that, can you tell us a little bit about your use case? Although this is an escape hatch to support it, we sort of consider async loading of data to be an anti-pattern, so I'm keen to hear about use cases to change my mind on that.

@agrohs
Copy link

agrohs commented Oct 14, 2020

Thanks @tmeasday, that's exactly what I did and seems to be working as expected now...cheers!

@agrohs
Copy link

agrohs commented Oct 14, 2020

one minor detail/question is in trying to prevent the loader from running again if its data has already been loaded (or not changed)? Something similar to the following (though it looks like as of now, we don't have access to loaded in the initial payload of the storyContext sent into the loader??)

export const loaders = [async ({ loaded: { activeContext: previousContext }}) => {
  if (previousContext) {
    return previousContext
  }
  return Promise.resolve({ activeContext: { foo: bar } })
}

export const decorators = (Story, { loaded: { activeContext } }) => {
  return (
    <StatefulProvider value={activeContext}>
        <Story {...storyContext} />
    </StatefulProvider>
  )
}

@tmeasday
Copy link
Member

tmeasday commented Oct 14, 2020

Hmm, I would probably suggest using some external cache here -- I don't know if SB should be doing the caching -- we pass the full context into the loader so for instance the loader could fetch different data depending on the context.id. So I don't really think we can do anything sensible in terms of caching the previous value without introducing surface area for problems.

You could do something as simple as using a global var here:

let previousContext;
export const loaders = [async ({ loaded: { activeContext: previousContext }}) => {
  if (previousContext) {
    return previousContext
  }
  return Promise.resolve({ activeContext: { foo: bar } }).then(v => { previousContext = v; return v });
}

@ZoltanT-RD
Copy link

Is this something that should work on globalTypes export as well, or it's only mean for decorators?
I'm trying to get this to work for globalTypes to load in the available localisations for the Toolbar, but can't seem to get this to work, it never seems to even hit the loader function =S

export const globalTypes = ({ loaded: { loadeditems } }) => ({
    locale: {
        name: 'Locale',
        description: 'Internationalization locale',
        defaultValue: 'en',
        toolbar: {
            icon: 'globe',
            items: loadeditems
            //    [
            //        { value: 'en', right: '🇺🇸', title: 'EN' },
            //        { value: 'fr', right: '🇫🇷', title: 'FR' },
            //        { value: 'es', right: '🇪🇸', title: 'ES' },
            //    ]
        }
    }
});
globalTypes.loaders = [
    async () => {
        console.log("async here");
        let results = await getLocales();
        console.log(results);
        return {
            loadeditems: results
        }
    }
];```

@shilman
Copy link
Member Author

shilman commented Nov 23, 2020

This only works for stories, not globalTypes unfortunately

@Bilge
Copy link

Bilge commented Jun 24, 2021

It seems this does not allow live arg updates to be fed into the loaders, so loaders are not really equivalent to an async Story, as originally proposed in #10009.


Edit: Actually args are passed as the first parameter to the loader.

@tmeasday
Copy link
Member

I don't think the loader will run again if you change the arg using controls. Is that what what you are looking for @Bilge ?

@Bilge
Copy link

Bilge commented Jun 25, 2021

I don't think the loader will run again if you change the arg using controls.

It does, though.

@tmeasday
Copy link
Member

Oh, OK then!

@bewards
Copy link

bewards commented Sep 14, 2022

Does data injected from loaders become available to argTypes (in react)? I have a loader that returns data, gets passed to a decorator, then that additional data gets sent to the Story. When I debug the component, the additional top-level prop is available (string array), but it doesn't show as a control.

@ndelangen
Copy link
Member

There's currently no way to get data from loaders to show up as controls AFAIK, perhaps @tmeasday or @shilman would know?

@tmeasday
Copy link
Member

Nope they are sort of seperate concepts. You could make an arg that "shadows" the loader and prefer it in the render function for the story?

@bewards
Copy link

bewards commented Sep 16, 2022

If I define the top-level prop for my component as a type property that gets set as an arg.property from the decorator, it shows up as a control (type object). I couldn't find a way to make the string array become a select type control though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants