Isomorphic Redux app not registering Redux-Thunk? - javascript

I seem to have a weird bug. I'm currently using Redux isomorphically and am also including redux-thunk as the middleware for async actions. Here's what my store config looks like:
// Transforms state date from Immutable to JS
const transformToJs = (state) => {
const transformedState = {};
for (const key in state) {
if (state.hasOwnProperty(key)) transformedState[key] = state[key].toJS();
}
return transformedState;
};
// Here we create the final store,
// If we're in production, we want to leave out development middleware/tools
let finalCreateStore;
if (process.env.NODE_ENV === 'production') {
finalCreateStore = applyMiddleware(thunkMiddleware)(createStore);
} else {
finalCreateStore = applyMiddleware(
createLogger({transformer: transformToJs}),
thunkMiddleware
)(createStore);
}
// Exports the function that creates a store
export default function configureStore(initialState) {
const store = finalCreateStore(reducers, initialState);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('.././reducers/index', () => {
const nextRootReducer = require('.././reducers/index');
store.replaceReducer(nextRootReducer);
});
}
return store;
}
The weird part about this is that I don't think there's anything wrong with this file because my createLogger is applied just fine. It logs out all my actions and state, but the minute I return a function instead of an object in an action creator, the execution is lost. I've tried throwing in debugger statements, which never hit and reordering the middleware also doesn't seem to help.
createUser(data) {
// This `debugger` will hit
debugger;
return (dispatch) => {
// This `debugger` will NOT hit, and any code within the function will not execute
debugger;
setTimeout(() => {
dispatch(
AppActionsCreator.createFlashMessage('yellow', 'Works!')
);
}, 1000);
};
},
Has anyone experienced something like this before?

DOH! I wasn't dispatching the action. I was only calling the action creator. Gonna have to get used to that with Redux!
How I thought I was invoking an action:
AppActionCreators.createFlashMessage('some message');
How to actually invoke an action in Redux:
this.context.dispatch(AppActionCreators.createFlashMessage('some message'));
Where dispatch is a method provided by the Redux store, and can be passed down to every child component of the app through React's childContextTypes

Related

Simulate actioncable websocket receive in webdriverIo

Is there a way during webdriverio runtime to simulate an actioncable receive?
I am using a fork of the package action-cable-react called actioncable-js-jwt for Rails actioncable js connections. Both of these packages are no longer maintained, but actioncable-js-jwt was the only actioncable for react package I could find that supported jwt authentication. I am building an app in my company's platform and jwt authentication is required.
The problem I am running into is that I have a react component which dispatches a redux action to call an api. The api returns a 204, and the resulting data is broadcasted out from Rails to be received by the actioncable connection. This triggers a dispatch to update the redux store with new data. The component does actions based on new data compared to the initial value on component load, so I cannot simply just set initial redux state for wdio - I need to mock the actioncable receive happening.
The way the actioncable subscription is created is:
export const createChannelSubscription = (cable, receivedCallback, dispatch, channelName) => {
let subscription;
try {
subscription = cable.subscriptions.create(
{ channel: channelName },
{
connected() {},
disconnected(res) { disconnectedFromWebsocket(res, dispatch); },
received(data) { receivedCallback(data, dispatch); },
},
);
} catch (e) {
throw new Error(e);
}
return subscription;
};
The receivedCallback function is different for each channel, but for example the function might look like:
export const handleUpdateRoundLeaderWebsocket = (data, dispatch) => {
dispatch({ type: UPDATE_ROUNDING_LEADER, round: data });
};
And the redux state is used here (code snippets):
const [currentLeader, setCurrentLeader] = useState(null);
const userId = useSelector((state) => state.userId);
const reduxStateField = useSelector((state) => state.field);
const onChange = useCallback((id) => {
if (id !== currentLeader) {
if (id !== userId && userId === currentLeader) {
setShow(true);
} else {
setCurrentLeader(leaderId);
}
}
}, [currentLeader, userId]);
useEffect(() => {
onChange(id);
}, [reduxStateField.id, onChange]);
Finally, my wdio test currently looks like:
it('has info dialog', () => {
browser.url('<base-url>-rounding-list-view');
$('#my-button').click();
$('div=Continue').click();
// need new state after the continue click
// based on new state, might show an info dialog
});
Alternatively, I could look into manually updating redux state during wdio execution - but I don't know how to do that and can't find anything on google except on how to provide initial redux state.

How do I store a non serialisable variable in a react - redux state without breaking it if I really must (like saving a device connection via WebAPIs)

I keep getting the statement "do not save non-serializable variables in your state" in almost every google search result - But what happens when I really should?
Progect: I am building an app for deviceS connected via SerialPort (using SerialPort WebAPI).
I wish to save the connection instance since I use it throughout all my application and I am honestly tired of passing the instance down and up whenever I need it without react knowing to re-render data and display new data - which is important for me too.
Steps that I have done:
It was easy to ignore the non-serializable error using serializableCheck: false:
export default configureStore({
reducer: {
serialport: SerialPortDevicesReducer,
bluetooth: BluetoothDevicesReducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
thunk,
serializableCheck: false
}).concat(logger),})
But now I am facing the big problem:
Whenever I create a connection I get the object that handles that specific SerialPort device object that is connected.
deviceReducer: {
id: 1,
instance: SerialPort{[attr and methods here]},
...
}
Whenever I use methods like open(), write() or read() it changes the main connection instance object and breaks with that known error:
Error: Invariant failed: A state mutation was detected between
dispatches, in the path 'serialport.0.instance.readable'. This may
cause incorrect behavior
Since It's not serializable I cannot clone it (which I think is the reason?) and then re-assign it + I think cloning a connection instance will cause other device-connection issues.
I ended up writing the connect method case directly in the state with a "promise" new variable to handle the result.
// click in a react component
const handleConnect = () => {
try {
if ( dispatch(connect(device)) ) {
setActiveStep((prevActiveStep) => prevActiveStep + 1)
return true
}
}
catch (e) {
console.error("Device cannot connect: ", e)
}
}
// In a file that trigges dispatch() to the reduces
const connect = (deviceId) => async (dispatch, getState) => {
try {
dispatch({
type: "serialport/connect",
payload: deviceId
})
} catch(e) {
console.log(e)
}
}
// in reducer
const SerialPortDevicesReducer = (state = initialState, action) => {
switch (action.type) {
case 'serialport/connect':
try {
return {
...state,
[action.payload]: {
...state[action.payload],
promise: state[action.payload].instance.open({baudRate: 115200})
}
}
} catch (e) {
console.error("Cannot run promise inside reducer: ", e)
}
This is the only workaround I currently found. And this basically forces me to handle (maybe some complex) things in the reducer instead of just passing data to it. I tried applying the same for the write method:
// click in component
const handleExecute = (command) => {
try {
dispatch(writeToSP(device1.device, command))
} catch (e) {
console.log(e)
}
}
// In file which trigges the dispatch()
const writeToSP = (deviceId, command = "Z !\n") => async (dispatch) => {
let startTime = new Date().getTime()
let encoder = new TextEncoder()
try {
dispatch({
type: "serialport/write",
payload: {
id: deviceId,
// cmd: encoder.encode(command),
// startTime
}
})
} catch (e) {
console.error("error writing: ", e)
}
}
// in reducer
...
case 'serialport/write':
try {
const writer = state[action.payload.id].instance.writable.getWriter()
} catch (e) {
console.error("Cannot run promise inside reducer: ", e)
}
and again, get the error of "Error: Invariant failed: A state mutation was detected..." which I am guessing a result of it changing other attributes in the SerialPort instance.
Having packages like redux-promise-middleware are awesome, but it seems like an object in my state is the one responsible for its own promise and changes.
How do I handle this specific situation?
Simple: don't put it into Redux. Redux is made for data, not for arbirtary external libraries/dependency injection.
If that value will never change after initialization and you do the initialization outside of React, just put it into a global variable that is exported.
If that value will change over time, you should use the Dependency Injection mechanism of React for it: Context. This is really what context is made for - not sharing state values, but global dependencies.

Nuxt Composition API, updating 'state', not reflected on UI Template

I had a Nuxt.js application working with the options API. And with the new Nuxt3 coming out, I was trying to migrate things over to the supposedly 'better' alternative. So far i've had nothing but challenges, perhaps that's my lack of knowledge.
I'm building a basic E-Commerce platform with a component of
# products/_id.vue
<template>
<div>
{{ product }}
</div>
</template>
<script>
import {
defineComponent,
useFetch,
useStore,
useRoute,
ssrRef, reactive, watch
} from '#nuxtjs/composition-api'
export default defineComponent({
setup () {
const store = useStore()
const route = useRoute()
const loading = ref(false)
// LOAD PRODUCT FROM VUEX STORE IF ALREADY LOADED
const product = reactive(store.getters['products/loaded'](route.value.params.id))
// GET PAGE CONTENT
const { fetch } = useFetch(async () => {
loading.value = true
await store.dispatch('products/getOne', route.value.params.id)
loading.value = false
})
// WATCH, if a use navigates to another product, we need to watch for changes to reload
watch(route, () => {
if (route.value.params.id) {
fetch()
}
})
return {
loading
product
}
}
})
</script>
One thing I need to note, is, if the product gets a comment/rating, I want the UI to update with the products star rating, thus needing more reactivity.
I continue to get an undefined product var
Inside my VueX store I have my getters
loaded: state => (id) => {
try {
if (id) {
return state.loaded[id]
}
return state.loaded
} catch {
return {}
}
}
Looking for directions on how to get this to work, improve any of the code i've currently setup.
If you want to maintain reactive referece to your getter, then you have to create a computed property.
So, what you return from your setup function is
product: computed(() => getters['products/loaded'](route.value.params.id))
This will make sure that whenever the getter updates, your component will receive that update.
Also, if the product already exists, you should bail out of the fetch function. So that you do not make the extra API call.
And, finally, if there is an error, you could redirect to a 404 error page.
All in all, your setup function could look something like this
setup() {
const route = useRoute();
const { error } = useContext();
const { getters, dispatch } = useStore();
const loading = ref(false);
const alreadyExistingProduct = getters['products/loaded'](route.value.params.id);
const { fetch } = useFetch(async () => {
// NEW: bail if we already have the product
if (alreadyExistingProduct) return;
try {
loading.value = true;
await dispatch('products/getOne', route.value.params.id);
} catch {
// NEW: redirect to error page if product could not be loaded
error({ statusCode: 404 });
} finally {
loading.value = false;
}
});
watch(route, () => {
if (route.value.params.id) {
fetch();
}
});
return {
loading,
// NEW: computed property to maintain reactive reference to getter
product: computed(() => getters['products/loaded'](route.value.params.id)),
};
},
You will probably also run into this harmless issue FYI.

How should I log every dispatch call to a Redux store?

I want to debug my Redux application by logging every action sent to the dispatcher in my the console.
What is generally regarded as the best approach for doing this?
You can use a simple logging middleware, like the example in the redux middleware docs:
const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd()
return result
}
Or use the redux-logger module, which basically does the same thing.
With TypeScript, use this annotation (where TStore is the type-name of the root of your state:
import { Middleware } from 'redux';
// ...
const logger: Middleware<{},TState> = store => next => action => {
console.group(action.type)
// etc

Dependency Injection in a redux action creator

I'm currently building a learner React/Redux Application and I can not wrap my head around how to do dependency injection for services.
To be more specific: I have a BluetoothService (which abstracts a 3rd Party Library) to scan for and connect to other devices via bluetooth. This service gets utilized by the action creators, something like this:
deviceActionCreators.js:
const bluetoothService = require('./blueToothService')
function addDevice(device) {
return { type: 'ADD_DEVICE', device }
}
function startDeviceScan() {
return function (dispatch) {
// The Service invokes the given callback for each found device
bluetoothService.startDeviceSearch((device) => {
dispatch(addDevice(device));
});
}
}
module.exports = { addDevice, startDeviceScan };
(I am using the thunk-middleware)
My Problem however is: how to inject the service itself into the action-creator?
I don't want that hard-coded require (or importin ES6) as I don't think this is a good pattern - besides making testing so much harder. I also want to be able to use a mock-service while testing the app on my work station (which doesn't have bluetooth) - so depending on the environment i want another service with the same interface injected inside my action-creator. This is simply not possible with using a static import.
I already tried making the bluetoothService a parameter for the Method itself (startDeviceScan(bluetoothService){}) - effectively making the method itself pure - but that just moves the problem to the containers using the action. Every container would have to know about the service then and get an implementation of it injected (for example via props).
Plus when I want to use the action from within another action I end up with the same problem again.
The Goal:
I want to decide on bootstrapping time which implemenation to use in my app.
Is there a good way or best practice for doing this?
React-thunk supports passing an arbitrary object to a thunk using withExtraArgument. You can use this to dependency-inject a service object, e.g.:
const bluetoothService = require('./blueToothService');
const services = {
bluetoothService: bluetoothService
};
let store = createStore(reducers, {},
applyMiddleware(thunk.withExtraArgument(services))
);
Then the services are available to your thunk as a third argument:
function startDeviceScan() {
return function (dispatch, getstate, services) {
// ...
services.bluetoothService.startDeviceSearch((device) => {
dispatch(addDevice(device));
});
}
}
This is not as formal as using a dependency-injection decorator in Angular2 or creating a separate Redux middleware layer to pass services to thunks---it's just an "anything object" which is kind of ugly---but on the other hand it's fairly simple to implement.
You can use a redux middleware that will respond to an async action. In this way you can inject whatever service or mock you need in a single place, and the app will be free of any api implementation details:
// bluetoothAPI Middleware
import bluetoothService from 'bluetoothService';
export const DEVICE_SCAN = Symbol('DEVICE_SCAN'); // the symbol marks an action as belonging to this api
// actions creation helper for the middleware
const createAction = (type, payload) => ({
type,
payload
});
// This is the export that will be used in the applyMiddleware method
export default store => next => action => {
const blueToothAPI = action[DEVICE_SCAN];
if(blueToothAPI === undefined) {
return next(action);
}
const [ scanDeviceRequest, scanDeviceSuccess, scanDeviceFailure ] = blueToothAPI.actionTypes;
next(createAction(scanDeviceRequest)); // optional - use for waiting indication, such as spinner
return new Promise((resolve, reject) => // instead of promise you can do next(createAction(scanDeviceSuccess, device) in the success callback of the original method
bluetoothService.startDeviceSearch((device) => resolve(device), (error) = reject(error)) // I assume that you have a fail callback as well
.then((device) => next(createAction(scanDeviceSuccess, device))) // on success action dispatch
.catch((error) => next(createAction(scanDeviceFailure, error ))); // on error action dispatch
};
// Async Action Creator
export const startDeviceScan = (actionTypes) => ({
[DEVICE_SCAN]: {
actionTypes
}
});
// ACTION_TYPES
export const SCAN_DEVICE_REQUEST = 'SCAN_DEVICE_REQUEST';
export const SCAN_DEVICE_SUCCESS = 'SCAN_DEVICE_SUCCESS';
export const SCAN_DEVICE_FAILURE = 'SCAN_DEVICE_FAILURE';
// Action Creators - the actions will be created by the middleware, so no need for regular action creators
// Applying the bluetoothAPI middleware to the store
import { createStore, combineReducers, applyMiddleware } from 'redux'
import bluetoothAPI from './bluetoothAPI';
const store = createStore(
reducers,
applyMiddleware(bluetoothAPI);
);
// Usage
import { SCAN_DEVICE_REQUEST, SCAN_DEVICE_SUCCESS, SCAN_DEVICE_FAILURE } from 'ACTION_TYPES';
dispatch(startDeviceScan([SCAN_DEVICE_REQUEST, SCAN_DEVICE_SUCCESS, SCAN_DEVICE_FAILURE]));
You dispatch the startDeviceScan async action, with the action types that will be used in the creation of the relevant actions. The middleware identifies the action by the symbol DEVICE_SCAN. If the action doesn't contain the symbol, it dispatches it back to the store (next middleware / reducers).
If the symbol DEVICE_SCAN exists, the middleware extracts the action types, creates and dispatches a start action (for a loading spinner for example), makes the async request, and then creates and dispatches a success or failure action.
Also look at the real world redux middle example.
Can you wrap your action creators into their own service?
export function actionCreatorsService(bluetoothService) {
function addDevice(device) {
return { type: 'ADD_DEVICE', device }
}
function startDeviceScan() {
return function (dispatch) {
// The Service invokes the given callback for each found device
bluetoothService.startDeviceSearch((device) => {
dispatch(addDevice(device));
});
}
}
return {
addDevice,
startDeviceScan
};
}
Now, any clients of this service will need to provide an instance of the bluetoothService. In your actual src code:
const bluetoothService = require('./actual/bluetooth/service');
const actionCreators = require('./actionCreators')(bluetoothService);
And in your tests:
const mockBluetoothService = require('./mock/bluetooth/service');
const actionCreators = require('./actionCreators')(mockBluetoothService);
If you don't want to specify the bluetooth service every time you need to import the action creators, within the action creators module you can have a normal export (that uses the actual bluetooth service) and a mock export (that uses a mock service). Then the calling code might look like this:
const actionCreators = require('./actionCreators').actionCreators;
And your test code might look like this:
const actionCreators = require('./actionCreators').mockActionCreators;
I created a dependency-injecting middleware called redux-bubble-di for exactly that purpose. It can be used to inject an arbitrary number of dependencies into action creators.
You can install it by npm install --save redux-bubble-di or download it.
Your example using redux-bubble-di would look like this:
//import { DiContainer } from "bubble-di";
const { DiContainer } = require("bubble-di");
//import { createStore, applyMiddleware } from "redux";
const { createStore, applyMiddleware } = require("redux");
//import reduxBubbleDi from "redux-bubble-di";
const reduxBubbleDi = require("redux-bubble-di").default;
const bluetoothService = require('./blueToothService');
DiContainer.setContainer(new DiContainer());
DiContainer.getContainer().registerInstance("bluetoothService", bluetoothService);
const store = createStore(
state => state,
undefined,
applyMiddleware(reduxBubbleDi(DiContainer.getContainer())),
);
const startDeviceScan = {
bubble: (dispatch, bluetoothService) => {
bluetoothService.startDeviceSearch((device) => {
dispatch(addDevice(device));
});
},
dependencies: ["bluetoothService"],
};
// ...
store.dispatch(startDeviceScan);

Categories

Resources