9. What are we showing you tonight?
How we produce a consistent and theme-able UI
How we partition the Redux store for full decoupled dev of
micro frontends
How we integrate React/Redux micro frontends into Angular
10.
11. What is a micro frontend?
An encapsulated piece of the front
end, defined by a domain concept
Able to run on it’s own with limited
setup
Published as an npm library that
exports a top-level component
17. Redux Crash Course
The dispatched actions are passed to reducer functions.
These reducers update the state
18. Redux Crash Course
UI components that are subscribed to the state changes will
re-render
19.
20.
21.
22.
23. Decoupling Redux micro frontends
Avoid action cross talk - actions should not
unintentionally update other micro frontends
Micro frontends define and use their own state
Micro frontends are ignorant of state structure
outside of their own state
25. A micro frontend is always passed the entire store when it is wrapped in
<Provider> by the parent
State structure decoupling
It needs a way to navigate to its part of the state without knowing the
structure
So to start, who is IOOF? Lets see if you can guess what IOOF stands for.
IOOF is primarily a superannuation company.
It’s also the biggest independent financial services company in australia not tied to a bank.
We have offices in every major city and software engineering teams in Melbourne, Sydney, Hobart and Adelaide.
Over the past couple of years, we’ve been rebuilding some of our front ends to in an effort to provide a better experience for our customers.
Let me take you through the journey we’ve been on…
In early 2015, in our biannual innovation day (which is our equivalent of a hackathon), one of the teams threw together an Angular app which became the basis of our new Members application
This is used by superannuation members to access and manage their account.
6 months later, we started building our next gen financial adviser platform which started as a fork of the Member app because they had overlapping functionality.
The Member app went live soon after in sept 2015 and here’s what it looked like
It’s a Angular single page app.
This is the IOOF branded app, but it also supports whitelabeling so we can create branded versions of the site for other employer groups.
Emily and I started in January this year.
At this stage there was very much stand alone teams for the Adviser and Member apps
They were both pure Angular apps.
However there were 2 other teams working on front ends one of which was building a react/flux app. This team pushed for us to start using NPM and webpack which we introduced in Feb. This got us off bower and gave us a consistent way for teams to publish and integrate their javascript packages.
At this stage we knew we wanted to move towards React, but we hadn’t really settled on state management and our integration strategy.
In April, we settled on Redux and came up with an integration strategy, which is part of what we’re going to talk about tonight. This strategy allowed us to scale to 8 teams all building react and redux micro frontends concurrently, that are assembled into a single page app.
Our adviser app is currently in pilot phase with the official launch at the end of the month. Here’s a quick look at it.
Like the Member app, it also supports white labelling, which we’ll explain a bit later.
Tonight we’re showing you some of the techniques we used to scale the development of these apps across several teams.
More specifically, we’re going to talk to you about how we integrate react and redux into Angular.
How we partition the Redux store for decoupled development
How we produce a consistent and theme-able UI
If you’re not familiar with React or Redux, don’t worry, we’ll explain the fundamentals along the way.
So when we first starting developing microfrontends to be included in the adviser application, our architecture looked something like this. The original angular app became the “parent app.” It contained client-side routing as well as several key parts of the site that had already been built at this point. Separate teams started building microfrontends in react. These were published as npm modules and integrated into the adviser app at compile time, using webpack. This means the entire application was deployed together.
This allowed us to do two things right away:
We were able to scale development of the adviser front end out to several different teams that were all able to work relatively independently of each other
This pattern gave us a path for gradually reducing our dependency on Angular 1 without needing to do a major rewrite right away
We’ve used the term “micro frontend” a few times already, so I want to talk about what we mean but that. A micro front-end is an encapsulated piece of the front end, defined by a domain concept. An example is the “account settings” pages. It should be a decoupled as possible from other micro frontons and the parent application and able to run on its own with limited setup. There are lots of different ways you could approach building and integrating micro frontends. In our case at IOOF, we chose to publish each micro front-end as an npm library that exports a top level React component.
Here’s an example of what that looks like in the application. Everything below the top bar is actually the account settings module. The parents app handles rendering the top portion and importing the account settings component.
Now let’s look at some code. Inside the parent angular app we wrote a directive that takes in a react component. It renders the component using ReactDom. The important part is highlighted right here.
As we started building more complex components, we identified we needed a solution for dealing with application state. Another team had been experimenting with Redux successfully and so we decided to choose Redux to manage state across the whole application
Before we talk about how we did that, I want to talk through some redux fundamentals, just in case you haven’t worked with it before.
Who here has worked with React before?
Pretend we’re building a todo list app because the world definitely needs another js todo list app.
Now that we all know how redux works, lets talk about how we introduced it into our application. When we introduced redux we decided to use a single global store. The store is initialized by the parent angular app and passed to each react micro fronted.
We could have had each micro front-end maintain it’s own state, but we opted not to do this because using a single stop meant:
We didn’t have to worry about different micro frontends getting out of sync with each other.
Redux gave us some simple techniques for solving cross-cutting application concerns like analytics
One of the big advantages we got from choosing redux to manage our application state was that it worked nicely with both the angular and react portions of our application
So, here’s what it looked liked to render a micro-fronted in the parent app once we introduced react. Now each time a micro-fronted is rendered, it’s wrapped in the redux provider and passed the redux store.
Introducing redux also meant that the contract for what a micro-fronted has to export changed slightly in addition to exporting a react component, each micro-frontend also has to export a top level reducer. The parent application imports all of these reducers and combines them
So based on the way we’ve integrated the microfrontend in the previous slide, you end up with a state structure like this.
The yellow represents the parts of the state owned by the parent and the boxes represent each micro frontends state. This structure occurs as a consequence of reducer composition through the usage of combineReducers on the previous slide.
One of the key benefits of the micro frontend architecture is it provides clear boundaries between parts of the UI which helps when divvying up development across teams. However, tying everything together with a single global store can get in the way of this.
The problem we needed to solve at IOOF was how can we develop our micro frontends in isolation and then integrate them together using a single global store.
In order to do this, we had to address 3 things.
The first is to avoid cross talk of actions across micro frontends. You don’t want multiple micro frontends inadvertently handling each other’s actions.
Secondly, micro frontends need to define and access their own state. You don’t want them navigating into each others state.
Thirdly, micro frontends have to be ignorant of state structure outside of their own state. This is very important because a micro frontend with knowledge about the parent state structure results in coupling to that structure.
I’m going to step you through the techniques we used to solve these things.
So to prevent action cross talk, we use a very simple namespacing convention
So take this action creator, creating an action type of SUBMIT. If 2 micro frontends are expecting a SUBMIT action, they’ll interfere with each other.
To prevent this, we prefix the action types with the namespace of INVESTMENTS so that only the investments micro frontend handles this action.
Our second and third goals were about micro frontends using the part of the state they define and being ignorant of the rest of the state.
So the problem to solve here is that React components connected to redux are automatically passed the entire store. This is how Provider works.
This means that you have to first navigate from the root of the state, which is defined outside of the microfrontend, before accessing the micro frontends state. This structure is most likely created and maintained by another team.
Obviously, this means you now have the parent state structure hardcoded in your micro frontend. If the parent app decides to change the structure of the store, your micro frontend is busted.
The way we solved this initially was to abstract the location of the micro frontends state to a lookup map. We called it the Traversal map
The traversal map is a simple javascript map where the key is a shared javascript constant (which we call a LocatorKey) and the value is the dot notation to the location in the state tree.
It is initialised by the parent app with the Locator Key (exported by the micro frontend) imported by the parent app when initialising the traversal map.
Here’s what it would look like for our investments micro frontend,
Having the parent app initialise the traversal map gives it the freedom to rearrange the store structure without breaking any of the micro frontends.
It also allows 2 parent apps to have different store structures without micro frontends being impacted.
Here’s what the store looks like when we add the traversal map
Now that we have a traversal map defined, how do we use it?
We wrote our own little library called redux-traversal. Here’s an example of its usage.
The library exports a function called traverse, which we call here passing in the global state and the micro frontends locator key. The sub state is returned.
Cool, so now we have a way to avoid having to navigate from the root of the state in our micro frontends which means they are decoupled from any parent state structure.
So what happens if we want to share some state across micro frontends. For example, we might want to share the logged in user profile or some config.
The obvious answer would be to put it in a common part of the store where all micro frontends can access it. But how do we do this without creating coupling in the micro frontends.
The answer is to use the traversal map
However, there is a slight difference. Since all the micro frontends will be accessing shared state, the locator keys need to be shared.
To do this we created a shared library called redux-traversal-constants that all the micro frontends use.
The keys are imported where needed and passed into the call to traverse.
This is the complete store structure with our shared state and traversal map added
So this pattern was working great for us until we ran into a problem. Basically we had no way of nesting micro frontends without ending up with the coupling we had tried so hard to avoid.
If you want to be able to nest micro frontends in each other and maintain a logical store structure, like in this diagram, there is no way for advanced search to avoid having a hardcoded location to it’s state. It has to navigate first to the Investments state and then to it’s own state nested inside the investments state.
Instead of trying to bend the traversal map to our will, I incepted an idea into one of our hobart devs to look at creating a wrapper component similar to Provider that would provide the substate to a micro frontend instead of the global state.
Michael and his twin brother together came up with a library that we’re in the process of open sourcing called Redux Subspace. The name Subspace comes from the concatenation of substate and namespace.
Here’s a look at how it works
Start by importing subspace provider
In this example, pretend this is your root react component. Here, we’ve wrapped it in Redux provider and provided the global store.
Then inside, we have our investments micro frontend. It’s wrapped in our new Subspace provider.
Subspace provider has a mapState prop that lets you tell it which part of the state to make available to the micro frontend
The namespace prop lets you specify an actionNamespace that will automatically get applied to any action dispatched from inside the investments micro frontend.
For namespacing to work, we need to wrap the reducer in the namespaced function from redux-subspace.
So then if we go into our Investment micro frontend, we’ve got our nested advanced search micro frontend, also wrapped in subspace provider. It’s getting it’s state mapped in based off the investments sub state, not the global state
To use the state, we don’t have a call to traverse anymore because the micro frontend is passed its substate by substate provider.
This is great because it means the micro frontend only needs to care about the structure of the state that it defined.
So what about shared state. The traversal map gave us a really good way to abstract shared state locations. Subspace puts the root state onto the root property by convention which lets you continue to use a traversal map for shared state.
Redux subspace is the result of our journey figuring out how to decouple Redux development.
From where we started to where we are now, we’ve come a long way, but it just goes to show that software patterns are fluid and you should never expect to do the same thing for too long
So far we’ve talked through techniques that we used to allow several different teams to work on the same singe page app. Now I want to talk through some of the things we did to make sure the user experience stayed consistent throughout.
One of the first things we did when we started scaling development of Adviser out to multiple teams was create a react ui components library. You can see that here (it might look familiar because we used react story book). It contains buttons, loading indicators, and icons, anything we want to look the same no matter where you see it on the site.
The other thing we did to make sure the user experience look consistent was make sure we had single solution to deal with theming. The adviser site has to support two themes, an IOOF one that uses a lot of greens and another one that uses more grey and yellows. We wanted to to make sure each micro-fronted wasn’t implementing this on it’s one, so we wrote two custom reach components.
The first is the ThemeProvider. It takes a theme object as a prop and passes that to all of its children using React context.
If you’ve looked at the react docs at all you might be a bit wary of using context. They give all sorts of disclaimers about why you shouldn’t use context because the API is unstable and subject to change.
We we felt in this scenario it was a pretty clean option and this tweet kind of illustrates why.
We decided we fell nicely into the second if block and promised to write a HOC.
Here’s what it looks like to use the theme. To access to the theme, out primaryButton is wrapped in the themed HOC. All the themed component does is take the theme from the context and pass it to our PrimaryButton as a prop. This allows us to isolate our components from changes to the context API.
This PrimaryButton will show up primaryLight, whether that’s green or yellow
Here’s what that looks like, when we’re integrating our micro-frontends into the parent app. Now each time a micro-fronted is rendered, in addition to wrapping it in the redux provider, we wrap it in out custom theme provider and pass it the theme. This means that it’s the parent app’s responsibility to know what the theme should be and pass it to each micro frontend. The micro frontends don’t need to care what the theme is or even know that there are multiple themes, they just need to use whatever is passed in.
Here’s what that looks like in the site. You’re actually looking at the same page on the two different pages. Pretty much the only code needed to accomplish this is the two components you already saw.
With these techniques and quite a bit of team collaboration we were able to bring everything together into this nice user experience. Hopefully our users will never notice that this single site was actually built by 8 different teams distributed across 3 locations.