redux-beacon
redux-beacon copied to clipboard
Changing the event map in runtime
This is a...
- [ ] :beetle: Bug Report
- [x] :rocket: Feature Request
- [ ] :scroll: Documentation Request
Which version of Redux Beacon are you using?
- 2.0.3
Which target(s) are you using?
- [ ] Google Analytics
- [ ] Google Analytics (gtag)
- [ ] React Native Google Analytics
- [x] Google Tag Manager
- [ ] React Native Google Tag Manager
- [ ] Amplitude
- [ ] Segment
- [x] Other/Third Party: ...(Qubit)
:rocket: :scroll: What's missing from Redux Beacon that you'd like to add?
I wish I had a possibility to change event map (passed to the metareducer of my ngrx store) in runtime. Our team needs to adjust analytics events data without rebuilding the application
Can you help out?
- [ ] :star2: I am a legend and can get started on a pull request right away given the go-ahead.
- [x] :star: I am a superstar and would like to help out given some guidance.
- [ ] :disappointed: I won't be able to help out on this one.
huh, interesting. Are you doing something like:
if user x, track these analytics, else if user y, track these analytics
No. We need to modify the EventDefinitionsMap
in the application without rebuild. Sometimes we need to change an event definition and we just don't want to wait for a new release of our app to do that.
The only solution I see now is to fetch the configuration from the server as JSON (instead of using your powerful EventDefinitionsMap) via XHR. And create my own meta reducer to apply static definitions declared in that JSON.
@cwayfinder
Sorry for the late reply on this. Had a busy week work-wise last week, and am in the middle of a move so my evenings/weekends are a bit more busy than usual.
I'm not sold on adding support for this sort of behaviour to the main redux-beacon package. Mostly because it seems like a fairly unique use-case. Is that fair?
But...we should be able to design something pretty easily based on the existing createMiddleware
and createMetaReducer
functions.
Maybe something like:
const getEvents = (eventsMap: EventsMap | EventsMapper) =>
typeof eventsMap === 'function'
? action => flatten<EventDefinition>([eventsMap(action)])
: action => getEventsWithMatchingKey(eventsMap, action.type);
function createMiddlewareDynamic(
eventsMap: EventsMap | EventsMapper,
target: Target,
extensions: Extensions = {}
) {
// save the initial (default) events definitions map
let currentEventsMap = eventsMap;
// a function that we expose to update currentEventsMap
const updateEventsMap = (newEventsMap: EventsMap | EventsMapper) => {
currentEventsMap = newEventsMap;
};
// the middleware
const middleware = store => next => action => {
const prevState = store.getState();
const result = next(action);
const nextState = store.getState();
const events = createEvents(
getEvents(currentEventsMap)(action),
prevState,
action,
nextState
);
registerEvents(events, target, extensions, prevState, action, nextState);
return result;
};
return { middleware, updateEventsMap };
}
And you'd use the function like so:
const { middleware, updateEventsMap } = createMiddlewareDynamic(initialEventsMap, gtmTarget(), { logger })
// use updateEventsMap wherever
updateEventsMap(newEventsMap)
Also, the function above would require access to the following utility functions in the main redux-beacon package:
import createEvents from './create-events';
import getEventsWithMatchingKey from './get-events-with-matching-key';
import registerEvents from './register-events';
I don't think I expose these. So for now, if you want to test it out I would copy-paste the compiled versions of these functions. If you need help with that, let me know.
I'll like to add one use case for this feature. As Redux based application grows there are several solutions to make it load faster:
- have several apps
- code split modules/pages
In our company, we're doing 2. so in order to use the redux-beacon
we need to have the ability to lazy load the events reducers.
For initial app load we fetch only actions/thunks/components that are required for that page. When a user navigates to another page we lazy-load their required files.
If we are going to use redux-beacon
as is, we will introduce a dependency graph to all trackable actions that will cause including all that files with actions to the initial load chunk (we're using Webpack) and make the initial JS file quite big. So, having something similar to redux#replaceReducer will help to solve the issue.
Thanks all for the comments on this issue! I'll share my implementation for code splitting in pure JS based on https://github.com/dbartholomae/redux-dynamic-modules-beacon
class EventsManager {
#eventsMaps = [];
getEventsMap = action => {
const eventsMapByAction = this.#eventsMaps
.map(eventMapOrMapper =>
typeof eventMapOrMapper === 'function'
? eventMapOrMapper(action)
: eventMapOrMapper[action.type],
)
.flat();
return eventsMapByAction;
};
addEventsMap(eventMapOrMapper) {
this.#eventsMaps.push(eventMapOrMapper);
}
removeEventsMap(eventMapOrMapper) {
this.#eventsMaps = this.#eventsMaps.filter(map => map !== eventMapOrMapper);
}
}
import { createMiddleware } from 'redux-beacon';
function createManageableMiddleware(eventsMap, target, extensions) {
const manager = new EventsManager();
manager.addEventsMap(eventsMap);
// Setup the event middleware
const middleware = createMiddleware(manager.getEventsMap, target, extensions);
return {
middleware,
manager,
};
}
//enhance store with manageable middleware + attach events manager to the redux store in configureStore func
store.eventsManager = manageableMiddleware.manager;
// hook for attach/detach events
import { useStore } from 'react-redux';
import { useEffect, useRef } from 'react';
import { isNull } from 'lodash';
function useEventsMap(eventsMap) {
const store = useStore();
const eventsMapWasAttached = useRef(null);
useEffect(() => {
if (isNull(eventsMapWasAttached.current)) {
store.eventsManager.addEventsMap(eventsMap);
eventsMapWasAttached.current = true;
console.log(`useEventsMap, events map attached:`, eventsMap);
}
return () => {
if (eventsMapWasAttached.current) {
store.eventsManager.removeEventsMap(eventsMap);
eventsMapWasAttached.current = null;
console.log(`useEventsMap, events map detached:`, eventsMap);
}
};
}, []);
}
//usage example
import {trackEvent} from "@redux-beacon/google-analytics-gtag";
const eventsMap = {
[ON_ACTION_TYPE_CONST_SUBMIT_THIS_EVENT]: trackEvent(() => {...}),
};
function SomeComponent(){
//on did mount life cycle attach events from this map, and detach it on unmount
useEventsMap(eventsMap);
return (
...
)
}