10

Update 2 - Add minimal 'working' example showing the issue

I trimmed down the project as far as I could while still showing the issue, to allow people to try out ideas/debug if they're interested github:store_import_test

The error happens in: request.js

Note: I'm aware the bounty is about to expire, but I'll re-enable it if that happens. I do appreciate all ideas/help put out so far!

End update 2

Update 1 - purpose description:

I want to access a value from the store (that can change overtime) in a 'utility function'. According to the redux docs subscribe is a valid option.

End update

I'm trying to import my redux-store outside of a component (in request.js , see below) similar to: What is the best way to access redux store outside a react component?

However, all those solutions (including https://github.com/reactjs/redux/issues/776) don't work because my request.js tries to import the store before createStore() is called in store.js resulting in store being undefined.

My directory structure looks like this

 .  
 ├── app  
 │   ├── api  
 │   ├── network 
 │   |    └── request.js 
 │   ├── app.js  
 │   ├── store.js  
 ├── index.android.js  
 ├── index.ios.js

The index.android/ios.js are the entry points and just load app.js

index.android/ios.js

import App from './app/app'

app.js

import store from './store'
class App extends React.Component {
    render() {
        return (
            <Provider store={store}>
                    <RootStackNavigation />
            </Provider>
        )
    }
} 

store.js

...
const store = createStore(reducer, initialState, middleware())
export default store

request.js

import store from '../../store'
store.subscribe(listener)
...
const someFunc(){}
export default someFunc

My thoughts / what I tried / where I get lost

Note: the store import path in request.js is valid, double checked Note2: the store is usable in app.js and the remainder of the program

I would think that the import store from '../../store' in request.js would trigger the const store = createStore(reducer, initialState, middleware()) line but apparently it seems it does.

attempt 1

I tried to also export the store as such:

export const store = createStore(reducer, initialState, middleware())

and imported it in request.js as:

import {store} from '../../store

thinking maybe the 'clever' default loading does some magic I don't know/understand. Same error, undefined

attempt 2 add getStore() to store.js

let store = null
store = createStore(reducer, initialState, middleware())
export function getStore() {
    if (store == null) {
        store = createStore(reducer, initialState, middleware())
    }
    return store
}

export default store

Doesn't work, the arguments to createStore have not been initialised yet.

I must be misunderstanding the loading process or mixing it with that of python, but what's preventing this from working? Others seem to be using the same solution (see the above mentioned posts) successfully.

ixje
  • 360
  • 1
  • 3
  • 20
  • I think you are trying to use the state of the react (with Redux management) without using react, which will not happen. Have you consider putting the request as a middleware to the application ? – Panagiotis Vrs Oct 04 '17 at 07:38
  • if you look at the SO I linked, you'll see people successfully doing what I'm asking. e.g. this solution https://stackoverflow.com/a/38480550/3869515 I'm basically trying to do the same as that solution. Given their success I think it should be possible – ixje Oct 04 '17 at 08:04
  • in your example the listener function is missing i guess you forgot to paste it right ? – Panagiotis Vrs Oct 04 '17 at 08:12
  • I omitted that part because I tried to keep it as minimal as possible without losing too much information. I clarified it by adding '...' in between. The error message is along the lines of "can't call subscribe on undefined" and when debugging it shows that `store` is undefined. – ixje Oct 04 '17 at 08:15
  • Could you provide feedback about `listener` in `request.js`? – Daniel Mizerski Oct 04 '17 at 12:10
  • Why are we focusing on the `listener`? The problem is the `store` object on which I'm trying to call subscribe is `undefined`. `listener` is just a callback for `subscribe`, it's just a normal `function listener() {//do something now I know the store was updated}` – ixje Oct 04 '17 at 13:06
  • @justsome make sure you don't have any import cycles around the files (like request.js -> store -> request.js). Does store.js require either app.js or request.js? – Alexander Vitanov Oct 29 '17 at 00:52
  • @AlexanderVitanov I checked for your import cycle suggestion in `store.js` but there doesn't seem to be any i could see. All except for 2 packages in there that are imported are external packages. The remaining 2 are my combinedReducer and rootSaga (also no store/app imports in there). `store.js` does not require `app.js` or `request.js`. – ixje Oct 29 '17 at 18:25
  • @justsome what packages are you using? What middlewares? What if you remove all middlewares and remove all middleware imports? – Alexander Vitanov Oct 30 '17 at 16:09
  • The way you are doing it should work. I will also try this. But another suggestion or way to do this is when you get the "store" in app.js pass it to request.js through some method (say initModule(store) ) and then you can do your stuff there. In your app.js componentDidMount() function you can do this as it would be the safest place and you will know that store is initialised. I don't know whether it is a good practice to do it in this way or not. – Ankit Aggarwal Nov 02 '17 at 09:47
  • Just a thought but could you not keep polling for the value of imported `store` until it is not undefined ?? As I understand your problem is that the `store` variable is undefined as you are trying to access it before the store is populated. So wait until the store has some value and then proceed on with your code :) – Prasanna Nov 02 '17 at 14:51
  • @AlexanderVitanov sorry for the late reply. I actually did try your middleware suggestions but had not reported back as I wanted to create a minimal project showcasing the issue first. I've updated the post to include that minimal project (which has no middle ware imports) and unfortunately still breaks. – ixje Nov 04 '17 at 09:35

4 Answers4

3

Found the problem. I was right - it’s a cycle. In store.js you require reducers/index.js then navigation.js then ../navigators/RootNavigationConfiguration which requires Home.js which requires /api/network which requires request.js and it requires store which at this point is not initialized. Try moving the store.subscribe(listener) along with the listener function to store.js right before you export it. Don’t forget to remove import store.js from request.js

Alexander Vitanov
  • 4,074
  • 2
  • 19
  • 22
  • Good find on the cycle. I don't think it's viable to move all such scenario's to `store.js`. Also, in my current case the `listener()` is supposed to operate on data I need in `request.js`. Even if I move it for just this case, then I'd have to expose the extra data in `store.js`, import this in `request.js` and then we have another cycle, right? If you look at `request.js` then I want my `base_url` @ line 32 to point to the value of `rpcUrl`, in turn `rpcUrl` is to be modified by `listener()` if `network.net` in the store changes. Hope that still make sense. – ixje Nov 05 '17 at 10:49
  • In short I want to modify the `base_url` on the fly depending on the `network.net` value in my store. – ixje Nov 05 '17 at 10:50
  • @justsome what i was about to say is that this is anti pattern. Are you using Redux thunk? If so get the url from the store and pass it on to api methods. Another way I recommend is using redux saga and passing it down the api methods too. – Alexander Vitanov Nov 05 '17 at 15:21
  • I'm using redux-saga, but passing the `network.net` value from the store around for every network call seems ridiculous. The `store.subscribe()` function is there for a reason. The examples I linked in the main post shows people doing the exact same thing but not for changing the base_url but for adding a token. I guess I should look into how to avoid having to load `RootNavigationConfiguration` from my navigation reducer (if that's possible) – ixje Nov 05 '17 at 17:27
  • How about setting a timeout to store.subscribe()? – Alexander Vitanov Nov 05 '17 at 17:31
  • Oh i have an idea - set a flag in the store when the store is ready and set an interval in request.js to check if the flag is true and then call store.subscribe() – Alexander Vitanov Nov 05 '17 at 17:34
  • if I could read the store for a flag, then I would also be able to call `subscribe()` on it :-). I actually found a way by testing inside my `request()` if the `rpcUrl` is `null`. If so I call `store.subscribe(listener)` and `listener()` (to initialise). Because it's inside the function it doesn't get called until the first `request()` is called and that avoids the loading cycle issue. – ixje Nov 06 '17 at 12:23
  • You are right. Good point. I think we got way off topic and the actual undefined mystery was solved. Regarding the way of implementing this - it might be another question just to keep things separate – Alexander Vitanov Nov 06 '17 at 12:25
  • I don't think it's off topic. Identifying the cause of undefined is one, solving it is two. I'm marking your answer as correct as the cyclic import identification answered the 'why' it was happening of my question. After that it was just a small extra step to find a solution around it. thanks again for all the help – ixje Nov 06 '17 at 13:43
0

I feel that you want to make antipattern. Your needcase is whispering to my ears "saga", but let's help you as it is.

I want to give you closed issue from github, but I want to make it clear where "they" are getting store - import {store} from "store"; is exported const store = configureStore(); and configure store (to avoid missunderstanding) is returning redux.createStore(...).

After you know what I wrote - you can understand what "they" are writing there and there.

Daniel Mizerski
  • 1,123
  • 1
  • 8
  • 24
  • 1
    I don't understand the relationship to the linked github issues. I want to access a value from the store (that can change overtime) in a 'utility function'. According to the [redux docs](http://redux.js.org/docs/api/Store.html#subscribelistener) `subscribe` is a valid option. I'm not trying to dispatch actions or modify state like those issues linked try to do. – ixje Oct 04 '17 at 08:40
  • I understood it like that you want to create function that will do something on store update – Daniel Mizerski Oct 04 '17 at 09:21
  • No problem. I updated the question to add my extra explanation to hopefully avoid further confusion. `store.getState()...` does not work in `request.js` because `store` is `undefined` at that point. That's the actual problem I'm facing. Once that's solved I can do whatever I want :) – ixje Oct 04 '17 at 11:52
  • Go on and post another question, it's interesting to find out how to implement this – Alexander Vitanov Nov 06 '17 at 13:57
0

Did you write "../../store" in request.js, but your directory structure says it should be "../store".

Rajat Gupta
  • 1,864
  • 1
  • 9
  • 17
  • You're right. However, it's only wrong in my edit of the `tree` output as the `network` folder is actually a subfolder of `api`. The actual import is correct. I've updated the post to include a minimum example project showcasing the issue. – ixje Nov 04 '17 at 09:36
0

You can simply create your store in the App Component to reduce complications, and pass it through the to sub-components.

let store = createStore(reducers);

export class App extends Component {
    constructor() {
        super();
        this.state = store.getState();
        this.syncState = this.syncState.bind(this);
    }

    syncState() {
        this.setState(store.getState());         
    }

    componentDidMount() {
        this.setState(store.getState());
        const unsubscribe = store.subscribe(this.syncState);
    }

    render() {
        return (
            <Provider store={store}>
                <AppNavigator/>
            </Provider>
        )
    }
}

Your sub-components then need to import connect from react-redux.

import {connect} from 'react-redux';

export class SubComponent extends Component {
  //Implementation
  //this.props.myLocalCopy - gives you the value
  //this.props.someTask(data) - Updates the store and updates your props.
}

function mapStateToProps(state) {
    return {
        myLocalCopy: state.someStoreProperty
    };
}

function mapDispatchToProps(dispatch) {
     return {
         someTask: (data) => {
             dispatch(actionCreators.someTask(data));
         }
     }
}

export default connect(mapStateToProps, mapDispatchToProps)(SubComponent)

This will help you to synchronize your app state with store throughout the workflow. Hope that helps! :)

Edison D'souza
  • 4,551
  • 5
  • 28
  • 39