drcmda / react-contextual

πŸš€ react-contextual is a small (less than 1KB) helper around React 16s new context api
MIT License
642 stars 23 forks source link

How-To: populate store with async data #16

Open Twisterking opened 6 years ago

Twisterking commented 6 years ago

Hi guys!

I am somehow a bit lost on how to properly populate a store with async data coming from a HOC. I am running a Meteor app with a react frontend using react-meteor-data.

So basically I am using their HOCs like this:

export default withTracker(props => {
  // Do all your reactive data access in this method.
  // Note that this subscription will get cleaned up when your component is unmounted
  const handle = Meteor.subscribe('todoList', props.id);

  return {
    currentUser: Meteor.user(),
    listLoading: !handle.ready(),
    tasks: Tasks.find({ listId: props.id }).fetch() // this line actually populates the <Foo> Component with the tasks
  };
})(Foo);

What I want to do now, is populate my react-contextual store with these props.tasks, so I can use them in my child components. Somehow I just can't figure out how. I seem not to fully understand how to use HOCs and how to pass async data to a store.

Until now I did it like this: I don't have any lifecycle methods in a store which I could use - I used react's old context til now and simply did something like this:

getChildContext() {
    return {
        myTasksForChildComponents: this.props.tasks
    }
}

... and that was it. I could access this.context.myTasksForChildComponents in all the child components.

Please help guys! Thanks a bunch!

drcmda commented 6 years ago

@Twisterking your original solution seems interesting, ... the old context was stale, meaning the tasks can't update. Is that your intent? I am not familiar with meteor but it looks like it injects tasks into the wrapped component, if you want to propagate it like react used to do it, then do it with https://github.com/drcmda/react-contextual/blob/master/API.md#modulecontext

Simply put tasks into the context.Providers value.

Twisterking commented 6 years ago

The example above was not really from my codebase, I used the old context more for simple booleans and "app wide" objects like the currently loggedin user and stuff like that. So I would use the withTracker HOC in some parent component (maybe even <App/>) and then just "pass it down" to all children via context. I now want to do it properly using the new context api. Your example uses decorators - unfortunately I need IE (11) support - is there any way around that?

drcmda commented 6 years ago

Sure, just wrap it, the decorator is just sugar for

decorator(arguments)(Component)

So, in that case:

moduleContext()(Component)

If you use Babel, though, it is IE11 compatible.

Twisterking commented 6 years ago

EDIT:

Okay I got it to work! πŸ˜ƒ Thanks a bunch for your help!

One more thing: Is there any way to populate this moduleContext at multiple points in the app? So maybe the <App/> component stores the currently logged in user in this "store", while some child component (with his own children) stores some other data in the same store - of course while keeping the currently logged in user in the store. Is this possible?

drcmda commented 6 years ago

Sure thing, would you prepare a small, contained codesandbox? Using the old context api? We can work on it until you're happy with it.

Twisterking commented 6 years ago

Thanks a bunch! Providing a codesandbox is a bit tricky with all the Meteor dependencies but I will try my best to show you what I mean (some is pseudo code but I am sure you understand what I mean):

AppStore.js (my "global" Store using moduleContext):

import React from 'react';
import { Provider, moduleContext, createStore } from 'react-contextual'

export default moduleContext()(class myContext extends React.PureComponent {
    render() {
        const { context, children, currentUser, testVal } = this.props;
        return <context.Provider value={ {currentUser, testVal} } children={children} />
    }
});

My main <App/>:

import { moduleContext, Subscribe } from 'react-contextual'
import AppStore from '../stores/AppStore';

...

class App extends React.PureComponent {
    render() {
    return (
        <Router history={history}>
          <AppStore currentUser={this.props.currentUser}>
            <div id="app-root" className="root">
              <Switch>
                <Route exact path="/login" component={ LoginLayout } />
                <Route exact path="/recover-password" component={ LoginLayout } />
                <Route exact path="/reset-password/:token" component={ LoginLayout } />
                <Route exact path="/signup" component={ LoginLayout } />
                <Route exact path="/verify-email/:token" component={ LoginLayout } />

                <RouteLoggedIn path="/" name="home" title="Home" component={ Home } {...this.props} />
              </Switch>
            </div>
          </AppStore>
        </Router>
    );
  }
}

export default withTracker(() => {
  const loggingIn = Meteor.loggingIn()
  return {
    currentUser: Meteor.user(),
    loggingIn,
    authenticated: !loggingIn && !!Meteor.userId()
  }
})(App);

As you can see, I export the withTracker HOC, which passes the currentUser prop. This prop is then passed down to the <AppStore/>.

Now ... INSIDE the home component (<RouteLoggedIn path="/" component={ Home })/>), there is a component named <GamesList/>.

<GamesList/>:

import { Subscribe } from 'react-contextual'
import AppStore from '../stores/AppStore';

...

export default class GamesList extends React.Component {
    render() {
        return (
            <AppStore testVal="foobar"> // this should basically ADD the testVal to the Store, whike KEEPING currentUser inside the store
                <Subscribe to={AppStore}>
                    {props => {
                        console.log('GAMESLIST PROPS', props); // testVal is now set, but currentUser is now undefined
                        return (
                            <div className="gameslist-container">
                                <h1>LIST</h1>
                                <AddGame/>
                            </div>
                        )
                    }}
                </Subscribe>
            </AppStore>
        )
    }
}

I hope you understand what I mean - I need a global store, which I can access on multiple "levels" inside my app - including populating it with additional values (in this case: most parent <App/> component only sets the currentUser, while some child component, in this case <GamesList/>, sets additional vars, in this case testVal).

Does this work for you? Do you understand what I mean?

SIDENOTE:

Until now, I basically just had several context vars which I set on several stages in my app using the basic:

getChildContext() {
    return {
        myNewContextVar: "value"
    }
}

... and no matter where I was inside my app, as long as it was a child of the component, I could just do this.context. myNewContextVar and it worked. Of course: What I could do, is make several "stores" using moduleContext(), but this isn't really my goal - I would love one big store for everything, which can be populated from my withTracker HOCs at any stage.

drcmda commented 6 years ago

I see. It's a little let's say unconventional to fill a store with a hoc, so i'd like to ask, does meteor give you any other means of fetching stuff or does it have to be the hoc? I'm trying to wrap my head around how i'd solve this in any other store, like redux for instance, and i think we'd bump into the same problem everywhere.

Twisterking commented 6 years ago

Unfortunately there is no real other way. You can check the docs about Meteor-React-Data here: Meteor Guide

Obviously there are many components which receive very similar data - so instead of using a pretty much exact similar withTracker over and over again and instead of passing down all the data via props all the time, my idea was to have a global store. Do you have any other ideas on how to tackle this problem?! πŸ˜„