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) - javascript

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.

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.

What is the best way to fetch api in redux?

How to write best way to fetch api resource in react app while we use redux in application.
my actions file is actions.js
export const getData = (endpoint) => (dispatch, getState) => {
return fetch('http://localhost:8000/api/getdata').then(
response => response.json()).then(
json =>
dispatch({
type: actionType.SAVE_ORDER,
endpoint,
response:json
}))
}
is it best way to fetch api?
The above code is fine.But there are few points you should look to.
If you want to show a Loader to user for API call then you might need some changes.
You can use async/await the syntax is much cleaner.
Also on API success/failure you might want to show some notification to user. Alternatively, You can check in componentWillReceiveProps to show notification but the drawback will be it will check on every props changes.So I mostly avoid it.
To cover this problems you can do:
import { createAction } from 'redux-actions';
const getDataRequest = createAction('GET_DATA_REQUEST');
const getDataFailed = createAction('GET_DATA_FAILURE');
const getDataSuccess = createAction('GET_DATA_SUCCESS');
export function getData(endpoint) {
return async (dispatch) => {
dispatch(getDataRequest());
const { error, response } = await fetch('http://localhost:8000/api/getdata');
if (response) {
dispatch(getDataSuccess(response.data));
//This is required only if you want to do something at component level
return true;
} else if (error) {
dispatch(getDataFailure(error));
//This is required only if you want to do something at component level
return false;
}
};
}
In your component:
this.props.getData(endpoint)
.then((apiStatus) => {
if (!apiStatus) {
// Show some notification or toast here
}
});
Your reducer will be like:
case 'GET_DATA_REQUEST': {
return {...state, status: 'fetching'}
}
case 'GET_DATA_SUCCESS': {
return {...state, status: 'success'}
}
case 'GET_DATA_FAILURE': {
return {...state, status: 'failure'}
}
Using middleware is the most sustainable way to do API calls in React + Redux applications. If you are using Observables, aka, Rxjs then look no further than redux-observable.
Otherwise, you can use redux-thunk or redux-saga.
If you are doing a quick prototype, then making a simple API call from the component using fetch is good enough. For each API call you will need three actions like:
LOAD_USER - action used set loading state before API call.
LOAD_USER_SUCC - when API call succeeds. Dispatch on from then block.
LOAD_USER_FAIL - when API call fails and you might want to set the value in redux store. Dispatch from catch block.
Example:
function mounted() {
store.dispatch(loadUsers());
getUsers()
.then((users) => store.dispatch(loadUsersSucc(users)))
.catch((err) => store.dispatch(loadUsersFail(err));
}

State Variable triggers mutation error

I have the following pseudo-code in my store module
const state = {
users: []
}
const actions = {
addUsers: async ({commit, state}, payload) => {
let users = state.users // <-- problem
// fetching new users
for(let i of newUsersThatGotFetched) {
users.push('user1') // <-- really slow
}
commit('setUsers',users)
}
}
const mutations = {
setUsers: (state, { users }) => {
Vue.set(state, 'users', users)
}
}
Now - when I run this code, I get the following error Error: [vuex] Do not mutate vuex store state outside mutation handlers.
When I put strict mode to false - the error is gone - but the process-time is really, really slow - as if the errors still happen but without getting displayed.
The problem seems to be where I commented // <-- problem, because after I change that line to
let users = []
everything runs flawlessly, but I can't have that because I need the data of state.users
The problem is: users.push('user1'), this is the line that mutates the state.
Remove anything that mutates the state (writes or changes it) from actions and move that into a mutation.
addUsers: async ({ commit }, payload) => {
// fetching new users
commit('setUsers', newUsersThatGotFetched)
}
Then add the new users in the mutation.
const mutations = {
setUsers: (state, users) => {
state.users.concat(users);
// or if you have custom logic
users.forEach(user => {
if (whatever) state.users.push(user)
});
}
}
The reason it is slow is related to Strict mode
Strict mode runs a synchronous deep watcher on the state tree for detecting inappropriate mutations, and it can be quite expensive when you make large amount of mutations to the state. Make sure to turn it off in production to avoid the performance cost.
If you want to speed up the mutation, you could do the changes on a new array which would replace the one in the state when ready.
const mutations = {
setUsers: (state, newUsers) => {
state.users = newUsers.reduce((users, user) => {
if (whatever) users.push(user);
return users;
}, state.users.slice()); // here, we start with a copy of the array
}
}

Vuex and mysql connection object: Do not mutate vuex store state outside mutation handlers

I've got an electron application that uses mysql package to connect to my database directly. What I'm trying to do is to store connection object created with mysql.createConnection() in the Vuex state. Then I want to fetch some numbers from the database using this object.
I got this code for Vuex store:
const state = {
connectionObject: null,
numbers: [],
};
const getters = {
getNumbers: state => state.numbers,
getConnectionObject: state => state.connectionObject,
};
const mutations = {
SET_NUMBERS(state, numbers) {
state.numbers = Object.assign({}, numbers);
},
SET_CONNECTION_OBJECT(state, connection) {
state.connectionObject = connection;
},
};
const actions = {
fetchNumbers({ state, commit }) {
const connection = _.cloneDeep(state.connectionObject);
connection.query({
sql: 'SELECT * FROM `numbers`',
timeout: 40000,
}, async (error, results) => {
if (error instanceof Error) {
throw new Error(error);
}
commit('SET_NUMBERS', await results);
});
},
connectToDatabase({ commit }, data) {
const connection = mysql.createConnection(data);
return new Promise((resolve, reject) => {
connection.connect(async (err) => {
if (err) {
return reject(err);
}
commit('SET_CONNECTION_OBJECT', await connection);
commit('SET_DB_CONNECTION_STATE', data);
return resolve(connection);
});
});
},
};
What happens is once I run the code I get Error in callback for watcher "function () { return this._data.$$state }": "Error: [vuex] Do not mutate vuex store state outside mutation handlers." and this error is thrown when connection.query() is executed.
I have a hard to time understand why it doesn't work as I clone connectionObject from state after all. Seem like its observers get cloned as well. Is there a way to avoid it?
The connection object is highly mutable, it can not be stored. Every connection you make does change properties of the connection object. I would only store the returns in the storage, the data update in to the actions and the connectionobject should be made accessable across vuex, beware making it global though, this would make your app easier to hack. One way to make it accessable is by simply declaring it a variable besides the state.
If you need further advice it would be greate to know which SQL package you used in order to give you some code.

Isomorphic Redux app not registering Redux-Thunk?

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

Categories

Resources