Redux Stores and componentize functionality

So I’ve been trying to wrap my head around how Redux works and looking at the cool work that the Andela team has been doing in the Order Entry OWA (https://github.com/openmrs/openmrs-owa-orderentry)…

As I mentioned in a previous thread, we’d like to start pulling out reusable components such has the header, patient header, etc. Pulling out the parts for rendering seems straightforward, but it gets more complicated when dealing with state.

For instance, the header interacts with the session by displaying the current user and the current session. We can extract the view into a separate module, but where do we handle fetching the current session? If we are using a single Redux store, how do we make sure that the different components don’t step on each others toes in terms of state?

Do we create a separate package that defines certain actions such as “fetchSession” which would add data to the store in specific locations? Then the header component would expect certain session values to be available in the store in specific places? This seemingly would rather tightly couple things and require some coordination across different React projects around where certain data should live in the store? Or is the there a simpler/better way to go about this? Is it possible we may not want to use Redux as all in this case?

fyi @larrystone and others…

Take care, Mark

1 Like

A good example of a library that needs to manage state with Redux is redux-forms. Basically, your library will export a reducer, which the user will then need to connect to their Redux store:

// store.js
import { createStore, combineReducers } from 'redux'
import { reducer as openMrsReducer } from 'openmrs-contrib-reactcomponents'

const rootReducer = combineReducers({
  // ...your other reducers here
  openmrs: openMrsReducer
})

Then, all your state will be in the Redux state tree under the openmrs key. For actually fetching the session, you’ll use redux-saga, which will dispatch actions with the data from the API, and your reducers will take care of updating the state with the new data.

Note that your state object can be as complex as you want.

1 Like

I had this thought while building the header component for the Order Entry OWA. One way I would have done this incase I don’t want to use redux would be to make API calls from the component and store it’s data within the component’s internal state. That way the component will be truly independent. Another way would be as suggested by @pascal above. I guess the main question is if we want the data from these individual components to be globally available to other components. Any thoughts @zeze @flavia @betty @geofrocker @fred @kodero ?

Thanks @larrystone and @pascal!

@larrystone, yes, one idea I thought of was not using Redux at all. Definitely would be interested in others thoughts.

The slightly annoying thing (at least to me) about exporting and importing the reducers is that you have to know/remember to import the reducers from ‘openmrs-contrib-reactcomponents’ when you create your store.

Anyway, I am playing around with that approach for now… I’m creating a simple action and reducer to fetch the session (using the Order Entry code as an example), and modified my “Hello World” header to display the session location, and I’m going to see if I can include that functionality into the Order Entry OWA using the pattern Pascal mentions.

I’d finishing up for the day, so I’m going to push up my changes to openmrs-contrib-reactcomponent but just a warning I’m not sure everything is currently working… :slight_smile:

One major change I made was to remove webpack (at least for now) and am just running babel on the code, based on the idea (from Pascal on a previous thread) that the consuming OWA will want to do it’s own production build and packing.

Take care! Mark

1 Like

I saw recently the following recording from the Boston React meetup last night where Michael Jackson (of React Training & React Router fame) spoke about the new Context API in React 16.3. You can watch it here on YouTube. https://el2.convertkit-mail.com/c/qdu9wllzvi7hv3km4/e0hph0/aHR0cHM6Ly95b3V0dS5iZS9abU1WV1o4dTY4VT90PTRtMXM=

I have not myself played with this, but it may solve some of these problems. So far my main use of redux was to pass state around to other components, without intermediate components needing to know about it. So with this model, the new (React 16.3) Context API looks useful. The context API is documented at https://reactjs.org/docs/context.html#when-to-use-context https://reactjs.org/docs/context.html#when-to-use-context

Tim.

@mogoodrich I agree with your opinions really. I guess we should stick to making the API calls directly from the component since we should also consider the pages (OWAs) that will be making use of these components might not need their values available to them. And yes, there is also a strong possibility that redux will be deprecated in favour of Context API in the future. So, I think it will be a plus if we can build components that can stand the test of time with little or no code breaking updates in the future.

@pascal just reading on all these various redux libraries… I’m assuming you would recommend redux-saga over redux-thunk? Seems like saga is considered an improvement?

Actually @larrystone I’m leaning towards using Redux at this point… or at least building a prototype of a header and see if we like how it works… lot’s of learning for me here. Also I am going to look into using redux-saga for handling asynchronous requests.

@darius do you know of any Thoughtworks opinions on this? I see that the Tech Radar recommends adopting React and Redux, couldn’t anything on react-saga.

Also anyone have any thoughts on GraphQL? (honestly, I don’t know anything about it, need to read up on it myself…)

Take care, Mark

Alright, today I worked a little more on the “openmrs-contrib-reactcomponents”. It’s taking me a little while to wrap my head around how to properly create and map reducers, but I managed to create an action and corresponding reducer that loads the session location and populates the store with the openmrs session under “openmrs.session”. To show that it works I changed the empty header to simply display the display name of the current location.

For testing, I tried incorporating this header into the Order Entry OWA. To do so I had to modify the redux-store.js file within the Order Entry OWA as follows (I’m not planning on committing this, just an example for now, as, among other things, it breaks existing functionality!):

import { createStore, compose, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { reducers as openmrsReducers } from 'openmrs-contrib-reactcomponents';
import reducers from './reducers';

const middlewares = [thunk];

const rootReducer = combineReducers({
  orderEntry: reducers,
  openmrs: openmrsReducers,
})


if (process.env.NODE_ENV !== 'production') {
  middlewares.push(logger);
}

export default () => {
  const store = createStore(rootReducer, compose(
    applyMiddleware(...middlewares),
    window.devToolsExtension && process.env.NODE_ENV !== 'production'
      ? window.devToolsExtension() : f => f,
  ));
  return store;
};

Note that following @pascal’s example it injects the reducers imported from openmrs-contrib-reactcomponents into the “openmrs” namespace and puts the reducers defined within the Order Entry OWA into a namespace called “orderEntry”. (So all the components with the OWA that map to reducers would need to be updated).

So, anyway, just an example of how things might work. I will continue to play around with things (though I’ve got some other tasks on my plate for this week so it might not be until next week).

Next steps for me, I think would be:

  • Demoing how components defined with another module (for example Order Entry) could consume reducers supplied by openmrs-contrib-reactcomponents
  • Explore react-saga as a alternative to react-thunk

Take care! Mark

You all are writing posts faster than I have time to think about them. Sorry I’m late to the party. :slight_smile:

Like Pascal says, the approach to this is to use combineReducers such that your store is partitioned into different modules.

You want to avoid accidentally touching another module’s state, but you sometimes want to touch it intentionally. Thus, consider namespacing actions, and also consider something like redux-subspace (and see this slideshare).

I would also expect that openmrs-contrib-reactcomponents is going to contain two kinds of things:

  1. global nav UI (e.g. the header, “your account”, the logout button), which we expect to be included in all apps, and has top-level state, and a top-level reducer.
  2. reusable widgets, which you can use elsewhere.

I assume that #2 is allowed to have internal state that redux doesn’t know about (e.g. an autocomplete widget can know if it is displaying results, and which one the user has keyed to before they click it), but I’ve never verified that this is good practice.

Thanks @darius! I will look at redux-subspace. I also foresee “openmrs-contrib-reactcomponents” containing some resources for accessing OpenMRS RESTful (though maybe those could/should be broken up into a separate module?). For instance an action and reducer that fetch the session from the server:

https://github.com/openmrs/openmrs-contrib-reactcomponents/blob/master/src/actions/sessionActions.js

https://github.com/openmrs/openmrs-contrib-reactcomponents/blob/master/src/reducers/sessionReducer.js

We’d want an OWA that needs session information to use this module. In order to do this in the current approach, the OWA would need to (I think):

  1. Import the reducers from openmrs-contrib-reactcomponenet and insert them when creating it’s store (as in my example above) within a specific part of the store (I’m using “openmrs” above)

  2. Trigger the action to make sure the session is loaded and put into th store

Though that should be straightfoward, there are some things about it that could get messy, though possibly coming up with conventions would solve it:

  • The OWA would to free to assign the part of the store it places the reducers in, but I think we’d want to define a convention, like “openmrs” above, so that when developers are working across OWAs they’d consistently know where to find things in the store provided by openmrs-contrib-reactcomponents. We could rely on convention, but I’m wondering if there’s a way to enforce this (from within the reactcomponents package)

  • Perhaps this is just a matter of good documentation, but we’d want to make it easy for developers using the reactcomponnents package to know where to find what they need and what format to expect it in. For instance, things like “if you dispatch the FETCH_CURRENT_SESSION action the default OpenMRS RESTful representation of the ‘session’ should be loaded and stored into the store at ‘openmrs.session’”. There’s an argument, perhaps, that the low-level fetch actions should generally just place the raw REST representations in the store for consistency? ie, if I issue a “FETCH_PATIENT” or " FETCH_ENCOUNTER", etc, I can expect the default REST representation of that object to be put in “openmrs.patient” or “openmrs.encounter”?

I’m just kind of doing stream-of-though design here, but it seems like that pattern might be a good one.

Thoughts?

Mark

Another tool that might help is redux-thunk. It allows you to make actions functions which are called by redux’s dispatch mechanism.

Since the reducer is required to be a pure function (with no side effects, or external inputs apart from its arguments), the reducer cannot make the REST call. With redux-thunk, you can create an action that is a function. This function can issue the REST call (as a fetch, perhaps) and then when the call finishes, issue the appropriate actions for the reducer.

So a fetch patient sequence might look like:

Action: const fetchPatient = (patientid) => { return (dispatch) => { dispatch({ type: PATIENT_LOADING }); fetch(patientfetchRESTURL, …). // Other arguments for the rest call .then(response => response.json(), () => { Do something with the error } ) .then(json => { dispatch({ type: PATIENT_DATA, json }); }); }; };

The reducer would then just handle the different simple actions: PATIENT_LOADING // Set some state property that indicates that the data is being loaded PATIENT_DATA // Load the patient from the json into the appropriate state properties // And clear the PATIENT_LOADING flag PATIENT_ERROR // Do something with the error and clear the PATIENT_LOADING flag.

@tansentim thanks! Looks like are currently using redux-thunk… but I like (and think we need) the pattern for error handling.

Also, redux-saga has been recommended as a possible successor/improve to thunk.

1 Like

I’d also highlight this

In a more complex app, you’re going to want different entities to reference each other. We suggest that you keep your state as normalized as possible, without any nesting. Keep every entity in an object stored with an ID as a key, and use IDs to reference it from other entities, or lists. Think of the app’s state as a database. This approach is described in normalizr’s documentation in detail. For example, keeping todosById: { id -> todo } and todos: array<id> inside the state would be a better idea in a real app, but we’re keeping the example simple.

@mogoodrich thanks for opening this thread and for your work on openmrs-contrib-reactcomponents. I find this super exciting. I have been reading the above comments with great interest, going back and forth from it and online Redux guides.

As you may know Attachments shall be refactored into an OWA as part of GSoC 2018. I wanted to stuck to Angular for the sake of keeping the scope smaller (since Attachments UI is already made in Angular), but of course I don’t want it to end up being out of sync with the community consensus if a whole React/Redux ecosystem is being developed.

I’m looking around and I quickly see that of course it is possible to do Angular + Redux (as everything is always virtually possible anyway). But I don’t want to do any contorsions at a juncture where maybe the safest approach would be to refactor to React and be 100% inline with the community efforts.

What’s everyone’s take here?

Cc @ridmal @zouchine

@mksd I would saw it depends on how much work it is… I think that just refactoring it into an OWA using Angular is fine… it looks like the consensus is likely to be React/Redux going forward, but even with that, there’s going to be older Angular code that is kept around (and maintained) for some time.

Without much research, I do think I agree that the Angular + React option is the least preferable one… if you really want to go with React, I would say refactor to React.

Take care, Mark

Hi @mogoodrich.

The order entry UI team have been trying to contribute to this project. However, we seem to be having a bit of challenge testing the components after creating them. For example, I have created an Accordion component but don’t know how to test it. I can see a sample folder but not sure how to make use of it for testing the newly created component. Also, is there a way to publish this to npm so we can also test its usage outside of the application?

Any help would be appreciated.

Update I managed to test the Accordion component I created but not without disabling bootstrap (because it requested jquery which should not be used in react). I also commented out the saga so as to focus on the actual component I intend testing.

cc @dkayiwa, @fred, @betty, @zeze, @flavia , @geofrocker, @darius, @kodero

I think we need to revisit the thought process behind this project based off the following questions:

  1. Does it mean that this project cannot be used with an existing project that uses redux-thunk instead of saga?

  2. Does it also mean that the user will have to compulsorily setup redux saga even if s/he won’t be making use of a component that requires such config eg the Accordion component? BTW Context API is coming which will eventually replace redux-thunk and redux-saga, what does that mean to the future of this project?

BTW Context API is coming which will eventually replace redux-thunk and redux-saga, what does that mean to the future of this project?

I’m not sure I understand this point. Thunks/sagas are a way to create async actions. The context API was introduced to make handling global state a bit cleaner (i.e. you don’t have to keep passing it down in props).

Does it also mean that the user will have to compulsorily setup redux saga even if s/he won’t be making use of a component that requires such config

You might be right about the ergonomics of this, but we might also be able to provide our own middleware that the user just adds and doesn’t have to think about. Either way, the setup should hopefully be minimal. Also, what’s the alternative here? If we’re picking one of thunk or saga to use in our library, the user is going to have to set that up.

Regarding (1), you can use thunks and sagas together, so that’s not really a problem.

p.s. I found it pretty interesting that the entire redux-thunk source code is only 14 lines long :slight_smile:

1 Like

@larrystone… thanks so much for contributing to this, great to have other people involved. As a broad response to your questions, I’ve definitely very new to React/Redux so there are certainly things that could be improved, so looking for others inputs

Some detailed points:

  • The user should not have to compulsorily set up redux saga if they are not using a component that requires the sagas. If you don’t set up the sagas does the accordian widget still work? Hopefully so, and this is just a matter of updating the documentation. Otherwise, we definitely want to make this possible.

  • I’d love it if the user didn’t have to “wire in” reducers or sagas at all and somehow they could just be automatically included as needed, but I didn’t really have a sense of how to do this. (If anyone does, I’m all ears)

  • I haven’t set up much testing in openmrs-react-components yet (and have some tickets on the board to do so). We should make it easy for people to write their own tests without having to disable things, etc. Could you issue a pull request with your Accordian changes and we can work through the way to make this happen?

Take care, Mark