Why is Thunk-Redux turning an object into a string? - reactjs

I've come across a strange issue with thunk-redux. I am writing a React-Redux app that calls a public API, and displays the data in a table. However, when I incorporated thunk middleware to handle the asynchronous API calls, my data is being stringified after the action is dispatched to the reducer.
index.js (action creator)
export const FETCHING_DATA = 'FETCHING_DATA';
export const FETCH_SUCCESS = 'FETCH_SUCCESS';
export const ERROR = 'ERROR';
export const getData = () => {
return {
type : FETCHING_DATA
}
}
export const getDataSuccess = (data) => {
return {
type : FETCH_SUCCESS,
payload: data
}
}
export const getDataFailure = () => {
return {
type : ERROR
}
}
export function searchCVE(cve){
const url = `${CVE_URL}api/cve/${cve}`;
return dispatch => {
dispatch(getData());
fetch(PROXY + url)
.then(blob => blob.json())
.then(data => {
console.log('Request: ', data);
dispatch(getDataSuccess(data))
})
.catch(e => {
console.log(e);
dispatch(getDataFailure(e.message))
});
}
}
data_reducer.js (reducer)
import {FETCHING_DATA ,FETCH_SUCCESS, ERROR } from '../actions/index.js';
const initialState = {
payload:[],
fetching: false,
error: false
}
export default function(state=initialState, action){
console.log('Got CVE: ', action);
switch (action.type){
case FETCHING_DATA: return {payload:[], fetching: true, ...state}
case FETCH_SUCCESS: return [action.payload, ...state]
case ERROR: return {payload:[], error: true, ...state}
}
return state;
}
As you can see in the index.js action creator, console.log('Request: ', data); displays the JSON object I want. However, when I {console.log('TEST: ' + this.props.cve)} in my table component, the console shows:
TEST: [object Object]
At no point in my app am I "stringifying" my data - why/where could thunk-redux be turning my data into a string? I thank the community for any insight it can provide.

At no point in my app am I "stringifying" my data - why/where could thunk-redux be turning my data into a string?
redux-thunk couldn't do that under no circumstances. It's deadly simple; all it's doing is processing function action in a different way.
The problem is that the object is being stringified by you, + addition operator coerces an object to a string:
{console.log('TEST: ' + this.props.cve)}
In case an object is expected to be displayed in the console, it should be:
{console.log('TEST: ', this.props.cve)}
Or it can be displayed in DOM:
<pre>{JSON.stringify(this.props.cve, null, 2)}</pre>

Related

Who to load dropdown options from API in react JS with typescript and react saga?

Here is my page, Here I want to load brand option from API.
I have written saga attached below:
Action.tsx
export const getBrandsForDropdown = (request: IPagination) => {
return {
type: actions,
payload: request
}
}
Api.tsx
export const getBrandsForDropdown = async () => {
const page = 1;
const limit = 1000;
console.log("get brand drop down");
const query = `user/master/brands?page=${page}&limit=${limit}`;
return client(query, { body: null }).then(
(data) => {
console.log("get brand drop down in ");
return { data, error: null };
},
(error) => {
return { data: null, error };
}
);
};
Reducer.ts
case actions.GET_BRANDS_DROPDOWN_PENDING:
return {
...state,
loading: true,
};
case actions.GET_BRANDS_DROPDOWN_REJECTED:
return {
...state,
loading: false,
};
case actions.GET_BRANDS_DROPDOWN_RESOLVED:
return {
...state,
loading: false,
brandOptions: action.payload,
};
Saga.ts
function* getBrandForDropDownSaga(action: HandleGetBrandsForDropdown) {
yield put(switchGlobalLoader(true));
yield put(pendingViewBrand());
try {
const { data } = yield getBrandsForDropdown();
yield put(resolvedViewBrand(data));
yield put(switchGlobalLoader(false));
} catch (error) {
yield put(switchGlobalLoader(false));
return;
}
}
After this I don't how to call it in my page and get it as a options in brand dropdown
Original Answer: Just Use Thunk
You can do this with redux-saga but I wouldn't recommend it. redux-thunk is a lot easier to use. Thunk is also built in to #reduxjs/toolkit which makes it even easier.
There is no need for an IPagination argument because you are always setting the pagination to {page: 1, limit: 1000}
Try something like this:
import {
createAsyncThunk,
createSlice,
SerializedError
} from "#reduxjs/toolkit";
import { IDropdownOption } from "office-ui-fabric-react";
import client from ???
// thunk action creator
export const fetchBrandsForDropdown = createAsyncThunk(
"fetchBrandsForDropdown",
async (): Promise<IDropdownOption[]> => {
const query = `user/master/brands?page=1&limit=1000`;
return client(query, { body: null });
// don't catch errors here, let them be thrown
}
);
interface State {
brandOptions: {
data: IDropdownOption[];
error: null | SerializedError;
};
// can have other properties
}
const initialState: State = {
brandOptions: {
data: [],
error: null
}
};
const slice = createSlice({
name: "someName",
initialState,
reducers: {
// could add any other case reducers here
},
extraReducers: (builder) =>
builder
// handle the response from your API by updating the state
.addCase(fetchBrandsForDropdown.fulfilled, (state, action) => {
state.brandOptions.data = action.payload;
state.brandOptions.error = null;
})
// handle errors
.addCase(fetchBrandsForDropdown.rejected, (state, action) => {
state.brandOptions.error = action.error;
})
});
export default slice.reducer;
In your component, kill the brandOptions state and access it from Redux. Load the options when the component mounts with a useEffect.
const brandOptions = useSelector((state) => state.brandOptions.data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchBrandsForDropdown());
}, [dispatch]);
CodeSandbox Link
Updated: With Saga
The general idea of how to write the saga is correct in your code.
take the parent asynchronous action.
put a pending action.
call the API to get data.
put a resolved action with the data or a rejected action with an error.
The biggest mistakes that I'm seeing in your saga are:
Catching errors upstream.
Mismatched data types.
Not wrapping API functions in a call effect.
Error Handling
Your brands.api functions are all catching their API errors which means that the Promise will always be resolved. The try/catch in your saga won't have errors to catch.
If you want to catch the errors in the saga then you need to remove the catch from the functions getBrandsForDropdown etc. You can just return the data directly rather than mapping to { result: data, error: null }. So delete the whole then function. I recommend this approach.
export const getBrandsForDropdown = async () => {
const page = 1;
const limit = 1000;
const query = `user/master/brands?page=${page}&limit=${limit}`;
return client(query, { body: null });
}
If you want to keep the current structure of returning a { result, error } object from all API calls then you need to modify the saga to look for an error in the function return.
function* getBrandForDropDownSaga() {
yield put(switchGlobalLoader(true));
yield put(pendingGetBrands());
const { data, error } = yield call(getBrandsForDropdown);
if (error) {
yield put(rejectedGetBrands(error.message));
} else {
yield put(resolvedGetBrands(data));
}
yield put(switchGlobalLoader(false));
}
Mismatched Data Types
There's some type mismatching in your reducer and state that you need to address. In some places you are using an array IBrand[] and in others you are using an object { results: IBrand[]; totalItems: number; currentPage: string; }. If you add the return type IState to the reducer then you'll see.
There's also a mismatch between a single IBrand and an array. I don't know the exact shape of your API response, but getBrandsForDropdown definitely has an array of brands somewhere. Your saga getBrandForDropDownSaga is dispatching resolvedViewBrand(data) which takes a single IBrand instead of resolvedGetBrands(data) which takes an array IBrand[]. If you add return types to the functions in your brands.api file then you'll see these mistakes highlighted by Typescript.
Don't Repeat Yourself
You can do a lot of combining in your API and your saga between the getBrands and the getBrandsForDropdown. Getting the brands for the dropdown is just a specific case of getBrands where you set certain arguments: { page: 1, limit: 1000 }.
export interface IPagination {
page?: number;
limit?: number;
sort?: "ASC" | "DESC";
column?: string;
}
export const getBrands = async (request: IPagination): Promise<IBrands> => {
const res = await axios.get<IBrands>('/user/master/brands', {
params: request,
});
return res.data;
};
function* coreGetBrandsSaga(request: IPagination) {
yield put(switchGlobalLoader(true));
yield put(pendingGetBrands());
try {
const data = yield call(getBrands, request);
yield put(resolvedGetBrands(data));
} catch (error) {
yield put(rejectedGetBrands(error?.message));
}
yield put(switchGlobalLoader(false));
}
function* getBrandsSaga(action: HandleGetBrands) {
const { sort } = action.payload;
if ( sort ) {
yield put(setSortBrands(sort));
// what about column?
}
const brandsState = yield select((state: AppState) => state.brands);
const request = {
// defaults
page: 1,
limit: brandsState.rowsPerPage,
column: brandsState.column,
// override with action
...action.payload,
}
// the general function can handle the rest
yield coreGetBrandsSaga(request);
}
function* getBrandsForDropDownSaga() {
// handle by the general function, but set certain the request arguments
yield coreGetBrandsSaga({
page: 1,
limit: 1000,
sort: "ASC",
column: "name",
})
}
export default function* brandsSaga() {
yield takeLatest(HANDLE_GET_BRANDS, getBrandsSaga);
yield takeLatest(GET_BRANDS_DROPDOWN, getBrandForDropDownSaga);
...
}
CodeSandbox

How to use an action within reducer?

So I am making a connection to a MQTT broker via Redux. I have three actions, one making the connection, another one checking for error and one receiving the message.
Only the first one gets triggered and the other 2 do not trigger. The connection is successful.
Here is my code:
Actions
export const mqttConnectionInit = (topic) => {
return {
type: 'INIT_CONNECTION',
topic:topic
}
}
export const mqttConnectionState = (err = null) => {
return {
type: 'MQTT_CONNECTED',
payload: err
}
}
export const processMessage = (data) => dispatch => {
console.log('Receiving Message')
return {
type: 'MESSAGE_RECEIVED',
payload: data
}
}
Reducer
import { mqttConnectionState} from './mqttActions'
import { processMessage} from './mqttActions'
const initState = {
client: null,
err: null,
message : 'message'
}
const createClient = (topic) => {
const mqtt = require('mqtt')
const client = mqtt.connect('ws://localhost:9001');
client.on('connect', function () {
mqttConnectionState('MQTT_CONNECTED')
client.subscribe(topic, (err, granted) => {
if (err) alert(err)
console.log(`Subscribed to: ` + topic)
console.log(granted)
});
});
//messages recevied during subscribe mode will be output here
client.on('message', function (topic, message) {
// message is Buffer
console.log(message.toString())
processMessage({topic, message})
// client.end() will stop the constant flow of values
})
return client;
}
const mqttReducer = (state = initState, action) =>{
switch (action.type) {
case 'INIT_CONNECTION':
return {
...state,
client: createClient(action.topic)
}
case 'MQTT_CONNECTED':
return {
...state,
err: action.payload
}
case 'MESSAGE_RECEIVED':
return {
...state,
message: action.payload //payload:data
}
default:
return state
}
}
export default mqttReducer
Why mqttConnectionState and processMessage do not get triggered?
You can never call async logic from within a reducer! Your createClient method is entirely async logic, and so it cannot go in a reducer.
In addition, you should not put non-serializable values into the Redux store.
Instead, we recommend that persistent connections like sockets should go into middleware.

React, Immutable, fetch() - incorrect error handling leading to undefined state element?

I think I understand where the error is occurring but I am able to work out the correct handling flow for a Promise returned from fetch()
My Messages reducer module: -
import { fetchMessages } from '_helpers/api'
import { Map, fromJS } from 'immutable'
const FETCHING_MESSAGES = 'FETCHING_MESSAGES'
const FETCHING_MESSAGES_FAILURE = 'FETCHING_MESSAGES_FAILURE'
const FETCHING_MESSAGES_SUCCESS = 'FETCHING_MESSAGES_SUCCESS'
const ADD_MESSAGES = 'ADD_MESSAGES'
const ERROR_MESSAGE = 'There has been an error'
export const fetchAndHandleMessages = () => {
return (dispatch, getState) => {
dispatch(fetchingMessages())
fetchMessages()
.then((r) => {
if (!r.ok) {
dispatch(fetchingMessagesFailure(ERROR_MESSAGE))
}else{
return r.json()
}
})
.then((b) => {
dispatch(fetchingMessagesSuccess(b))
})
.catch(() => {
dispatch(fetchingMessagesFailure(ERROR_MESSAGE))
})
}
}
function fetchingMessagesSuccess(messages) {
return {
type: FETCHING_MESSAGES_SUCCESS,
messages,
lastUpdated: Date.now(),
}
}
function fetchingMessagesFailure(errMsg) {
return {
type: FETCHING_MESSAGES_FAILURE,
error: errMsg
}
}
const fetchingMessages = () => {
return {
type: FETCHING_MESSAGES,
}
}
const initialState = fromJS({
messages: [],
isFetching: true,
error: '',
})
export const messagesReducer = (state = initialState, action) => {
switch (action.type) {
case FETCHING_MESSAGES :
return state.merge({
isFetching: true,
})
case FETCHING_MESSAGES_SUCCESS :
return state.merge({
error: '',
isFetching: false,
messages: action.messages
})
case FETCHING_MESSAGES_FAILURE:
return state.merge({
error: action.error,
isFetching: false
})
default :
return state
}
}
export default messagesReducer
fetchMessages() simply returns a promise: -
export const fetchMessages = () => {
return fetch(baseUrl + 'messages')
}
I am not going to post the component code here because it is not relevant to the issue.
So if I call fetchMessages() with an invalid URL to return a 404, state.messages becomes undefined in my component. This would seem to be being caused by this part of the function: -
if (!r.ok) {
dispatch(fetchingMessagesFailure(ERROR_MESSAGE))
}else{
return r.json()
}
I think I might be confused regarding how to properly check and deal with potential errors in the returned Promise. According to the docs for fetch(), a 404 is not considered to be an error as (unlike regular AJAX) only network issues are considered to be a catch() type of error.
Can anyone pinpoint for me what is wrong with this part of my code? should I be using exit after dispatch(fetchingMessagesFailure(ERROR_MESSAGE)) to stop the following .then()? Also, even with just a 404, the .catch() block is also being run. This seems to be against what the docs suggest.
Any help greatly appreciated. Thanks.
I see you are using the same action on !r.ok and catch... so I would recommend to break the chain in case of !r.ok via throwing an error:
fetchMessages()
.then((r) => {
if (!r.ok) {
throw true; // just go to .catch()
}
return r.json()
})
.then((b) => dispatch(fetchingMessagesSuccess(b)))
.catch(() => dispatch(fetchingMessagesFailure(ERROR_MESSAGE)))

React Redux adding extra field for action cause promise to return differently

I want to add a isLoading flag to my action generator and reset it at my reducer. Initially without the flag, my code works and the action looks like the following
export function getList() {
const FIELD = '/comics'
let searchUrl = ROOT_URL + FIELD + '?ts=' + TS + '&apikey=' + PUBLIC_KEY + '&hash=' + HASH;
const request = axios.get(searchUrl)
return {
type: FETCH_LIST,
payload: request,
}
}
and reducer looks like
const INITIAL_STATE = { all: [], id: -1, isLoading: false };
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_COMIC_LIST:
console.log('reducer action =', action, 'state =', state)
return {
...state,
all: action.payload.data.data.results,
isLoading: false
}
default:
return state;
}
}
As you can see, the object returns fine and I can get my list via action.payload.data.data.results
Note that I am using redux promise as my middleware to handle the promise.
As soon as I changed my action to the following and re run the code I get my payload (as shown in image below) to be a Promise rather than the object that was returned
export function getComicList() {
const FIELD = '/comics'
let searchUrl = ROOT_URL + FIELD + '?ts=' + TS + '&apikey=' + PUBLIC_KEY + '&hash=' + HASH;
const request = axios.get(searchUrl)
return {
type: FETCH_COMIC_LIST,
isLoading: true,
payload: request,
}
}
Why is simply adding another variable causing this problem??
Redux Promise and Redux Promise Middleware are both compliant with Flux Standard Action (FSA). Add a meta key to the action and assign your key/value pairs within it.
Related question/answer: https://stackoverflow.com/a/35362148/4290174
From the FSA documentation:
The optional meta property MAY be any type of value. It is intended for any extra information that is not part of the payload.
Try this - its how its done using redux-thunk - so I'm hoping its similar to redux-promise-middleware. Also see what is returned from:
all: action.payload in your reducer
export function getList() {
const FIELD = '/comics'
let searchUrl = ROOT_URL + FIELD + '?ts=' + TS + '&apikey=' + PUBLIC_KEY + '&hash=' + HASH;
// return a promise that the middleware should handle
// return response or error from promise
return axios.get(url)
.then((response) => {
type: FETCH_LIST,
isLoading: true,
payload: response
}).error((response) => {
//handle error
});
}
I think the cleanest and most react-redux way is:
//types:
const FETCH_LIST_START = "…";
const FETCH_LIST_SUCCESS = "…";
const FETCH_LIST_ERROR = "…";
//action
const loadList = url => dispatch => {
dispatch({type: FETCH_LIST_START });
fetch(url)
.then(res => {
if (res.status !== 200) throw new Error('load failed');
dispatch({
type: FETCH_LIST_SUCCESS,
payload : { res } }
})
)
.catch(err => dispatch({
type: FETCH_LIST_ERROR,
payload: { error } })
);
};
//REDUCER
case: FETCH_LIST_START:
return Object.assign({}, state, { isLoading: true });
break;
case: FETCH_LIST_SUCCESS:
return Object.assign({}, state, {
isLoading: false,
data: action.payload.res
});
break;
case FETCH_LIST_ERROR:
…
break;
So that assumes you are using redux-thunk. The basic Idea is to make your reducer handle the state, including setting the isLoading thing, that way, you can update your component while the request state changes… The code above is not copy/paste ready, it is meant to transport the idea.

How to store client in redux?

I'm setting up a redux application that needs to create a client. After initialization, the client has listeners and and APIs that will need to be called based on certain actions.
Because of that I need to keep an instance of the client around. Right now, I'm saving that in the state. Is that right?
So I have the following redux action creators, but then when I want to send a message I need to call the client.say(...) API.
But where should I get the client object from? Should I retrieve the client object from the state? My understanding is that that's a redux anti-pattern. What's the proper way to do this with redux?
Even stranger – should the message send be considered an action creator when it doesn't actually mutate the state?
The actions:
// actions.js
import irc from 'irc';
export const CLIENT_INITIALIZE = 'CLIENT_INITIALIZE';
export const CLIENT_MESSAGE_RECEIVED = 'CLIENT_MESSAGE_RECEIVED';
export const CLIENT_MESSAGE_SEND = 'CLIENT_MESSAGE_SEND';
export function messageReceived(from, to, body) {
return {
type: CLIENT_MESSAGE_RECEIVED,
from: from,
to: to,
body: body,
};
};
export function clientSendMessage(to, body) {
client.say(...); // <--- where to get client from?
return {
type: CLIENT_MESSAGE_SEND,
to: to,
body: body,
};
};
export function clientInitialize() {
return (dispatch) => {
const client = new irc.Client('chat.freenode.net', 'react');
dispatch({
type: CLIENT_INITIALIZE,
client: client,
});
client.addListener('message', (from, to, body) => {
console.log(body);
dispatch(messageReceived(from, to, body));
});
};
};
And here is the reducer:
// reducer.js
import { CLIENT_MESSAGE_RECEIVED, CLIENT_INITIALIZE } from '../actions/client';
import irc from 'irc';
export default function client(state: Object = { client: null, channels: {} }, action: Object) {
switch (action.type) {
case CLIENT_MESSAGE_RECEIVED:
return {
...state,
channels: {
...state.channels,
[action.to]: [
// an array of messages
...state.channels[action.to],
// append new message
{
to: action.to,
from: action.from,
body: action.body,
}
]
}
};
case CLIENT_JOIN_CHANNEL:
return {
...state,
channels: {
...state.channels,
[action.channel]: [],
}
};
case CLIENT_INITIALIZE:
return {
...state,
client: action.client,
};
default:
return state;
}
}
Use middleware to inject the client object into action creators! :)
export default function clientMiddleware(client) {
return ({ dispatch, getState }) => {
return next => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
const { promise, ...rest } = action;
if (!promise) {
return next(action);
}
next({ ...rest });
const actionPromise = promise(client);
actionPromise.then(
result => next({ ...rest, result }),
error => next({ ...rest, error }),
).catch((error) => {
console.error('MIDDLEWARE ERROR:', error);
next({ ...rest, error });
});
return actionPromise;
};
};
}
Then apply it:
const client = new MyClient();
const store = createStore(
combineReducers({
...
}),
applyMiddleware(clientMiddleware(client))
);
Then you can use it in action creators:
export function actionCreator() {
return {
promise: client => {
return client.doSomethingPromisey();
}
};
}
This is mostly adapted from the react-redux-universal-hot-example boilerplate project. I removed the abstraction that lets you define start, success and fail actions, which is used to create this abstraction in action creators.
If your client is not asynchronous, you can adapt this code to simply pass in the client, similar to how redux-thunk passes in dispatch.

Resources