HenrikJoreteg / redux-bundler

Compose a Redux store out of smaller bundles of functionality.
https://reduxbundler.com
583 stars 46 forks source link

Passing arguments to selectors #36

Closed N8-B closed 5 years ago

N8-B commented 5 years ago

I'm currently working on a proof-of-concept refactor for a large application with redux-bundler and I'm loving it so far.

I understand that the correct (and only?) pattern for using selectors is to create select* functions that receive the entire state. I've tried different approaches to passing in dynamic parameters to the selector functions created in each bundle, but have not had any luck.

A possible use case for the app would be selecting a correctly formed endpoint path from a hash table that's stored in redux, e.g.

endpoints: {
  accounts: {
    baseUrl: 'accounts/V01'
  }
  ...
}

And in the bundle, I would like to do something like...

selectEndpointsRaw: state => state.endpoints,
selectEnpointsPath: (state, path) => path, <= Is this possible?
selectEndpointsByPath: createSlector(
  'selectEndpointsRaw',
  'selectEnpointsPath',
  // The 'getPath' method returns a correctly formed path for API calls:
  // If the path passed in is 'accounts.baseUrl', the selector
  // would return 'accounts/V01'
  (endpoints, path) => getPath(endpoints, path)
),

From what I can tell, I can only create selectors that receive the entire state. As I have just started using redux-bundler, perhaps I'm going about this all wrong.

How would you handle such a case with redux-bundler? Any pointers would be greatly appreciated.

Thanks and great work on this!

HenrikJoreteg commented 5 years ago

Hi Nathan, glad you're liking it so far.

You're right that they always receive the entire state. So if you want anything dynamic, that dynamic thing has to be replesented somehow in the state too. Often I do this with the application's current route/url. So if I'm on a detail page for an item, for example I might have a route like:

/Items/:itemId

Then I can use selectRouteParams in a selector you can do something like:

selectActiveItem: createSelector(
  'selectItemsRaw', // gets a hash of all by ID
  'selectRouteParams',
  (items, params) => {
    if(!params.itemId) return null
    return items[params.itemId]
  }
)

The I'd doesn't have to be from the URL, it could also be something else, like a user action that selects something but you store the selected ID in a reducer somehow and use the same technique to select the active item.

Hope that helps, and I hope I understood your question correctly :)

N8-B commented 5 years ago

Yeah, you understood my question correctly. That approach makes a lot of sense. I was just looking at the redux-bundler-example and saw that you used that pattern in the people bundle - great solution.

The trouble I'm having is that I'm attempting to adapt some helper methods from the current code base whose arguments (the endpoint to use for the request to the backend service) are provided by the developer and not the client. So, it's not something that could be easily represented in the state.

For now, I am doing the setup for the bundle state in the init method. I pick off some configuration variables (host paths) from the store, save the configured endpoints in the state and initialize an external helper module that has the getPath methods used by developerds. It's not ideal, as I'd likke to avoid having to use the external module, but it will work for the moment.

One of the biggest problems I'm encountering is trying to add redux into a large application without changing too much the internal logic of the application - which is likely the issue. It feels like I'm attempting to fit a square peg in a round hole.

Anyways, thank you very much for your help. I think I will take a step back, read your book again and think about a better approach.

Feel free to close this 'non'-issue whenever you wish.

HenrikJoreteg commented 5 years ago

The trouble I'm having is that I'm attempting to adapt some helper methods from the current code base whose arguments (the endpoint to use for the request to the backend service) are provided by the developer and not the client. So, it's not something that could be easily represented in the state.

It sounds a bit like you're talking about what essentially amounts to configuration? Which API endpoint to use in various environments? For this, I often use a simple config file at the root of my /src directory that looks something like this:

import { IS_BROWSER } from 'redux-bundler'

const defaultValues = {
  cacheVersion: 4,
}

const configs = {
  dev: {
    apiUrl: 'https://api-stage.example.com'
  },
  prod: {
    apiUrl: 'https://api.example.com'
  }
}

let env = 'dev'

if (IS_BROWSER && window.location.hostname === 'example.com') {
  env = 'prod'
}

export default Object.assign({}, defaultValues, configs[env])

then other files (such as perhaps an API helper that wraps fetch()) can just import the config file and read values:

import config from '../config'

export default (path, options) => fetch(config.apiUrl + '/' + path, options)

This allows you to build/test your app once and then deploy it to multiple environments. Sure, you're sending config values that aren't used, but none of this data should ever be sensitive anyway and most of the time there are only a handful of values that may change from one environment to the next.

N8-B commented 5 years ago

Yes, that is exactly what I'm talking about. We have 4 different environments which have their corresponding configurations ("generic" environment variables) and toggles (a hash of key-value pairs whose Boolean values are calculated based on the environment and are used to "activate/deactivate" functionality within the app).

The current setup is similar to what you have proposed. The required base url (apiUrl) is passed into the http-request wrapper we have and is transparent to the developers. The dynamic portion of the path is what is generated by the helper methods (getPath) imported and used during development in the modules.

Perhaps the best solution would be to remove the endpoint-paths module's internal dependency on the environment config value for the api urls and have it concentrate on building solely the dynamic path of the url.

I agree with you that including the config values in the final build is not a problem. We have them in different files to make generating the derived config value (toggles) easier to handle.

Thanks for your time and insight.

HenrikJoreteg commented 5 years ago

@N8-B no problem, good luck!

odinho commented 5 years ago

I've several times wondered about another problem though, which is the case of having a lot of children, where the children themselves only know their ID, and I would like to have a good way to extract info from a selector.

I guess a usable way is to return an object you can index on: selectLatestChatMessageByUser()[myUserID]. The only "problem" is that once one chat does update, -- every child will be re-rendered. I don't think that's a problem as long as you don't have too many children.

Just wanted to put that out there, since this is the second time I'm googling to check if there's a better way to do it.