Redux Sagas Recursion Not Working - redux

I want to find a item and its sub items by item id, and I write the following code, but fetchSubItems() always not working, and throwing exception 'TypeError: Cannot read property 'root' of undefined', anyone can help me?
export function *fetchItem(api, id){
const item = yield call (api.getItem, id)
yield put(Actions.addItem(item))
yield call(fetchSubItems, item)
yield put(Actions.success())
}
export function *fetchSubItems(api, item){
if(item.children){
const children = yield item.children.map((id)=>{
return call(api.getItem, id)
})
yield put(Actions.addItems(children))
// the following lines throws 'TypeError: Cannot read property 'root' of undefined'
yield children.map((child)=>{
call(fetchSubItems, api, child)
})
}
}

It seems that a return statement is missing in the last call. The working example:
import Promise from 'bluebird';
import { delay } from 'redux-saga';
import { call } from 'redux-saga/effects';
import {
reducer
} from '../reducers/counter';
import { logger } from '../utils';
const name = '19/Tree_Traversal';
const log = logger(name);
const delayTime = 10;
const tree = {
1: {children: [2, 3]},
2: {children: [4, 5, 6]},
3: {children: []},
4: {children: []},
5: {children: []},
6: {children: [7]},
7: {children: []}
};
const api = {
getItem(id) {
log(`getItem(${id})`);
return delay(delayTime, tree[id]);
}
};
export function *fetchItem(/*api, */id = 1) {
const item = yield call(api.getItem, id);
// yield put(Actions.addItem(item))
yield call(fetchSubItems, /*api, */item);
// yield put(Actions.success())
}
export function *fetchSubItems(/*api, */item) {
if (item.children) {
const children = yield item.children.map((id) => {
return call(api.getItem, id);
});
// yield put(Actions.addItems(children))
yield children.map((child) => {
return call(fetchSubItems, child); // <=== added `return`
});
}
}
export default {
name,
saga: fetchItem,
reducer: reducer,
useThunk: !true,
execute(store) {
return Promise.delay(8 * delayTime)
.then(() => this);
}
};
returns the following log:
00000000: [counter reducer] action Object {type: "##redux/INIT"}
00000003: [Runner] ---------- running example 19/Tree_Traversal
00000004: [Runner] store initial state 0
00000008: [19/Tree_Traversal] getItem(1)
* 00000060: [19/Tree_Traversal] getItem(2)
00000061: [19/Tree_Traversal] getItem(3)
* 00000074: [19/Tree_Traversal] getItem(4)
00000074: [19/Tree_Traversal] getItem(5)
00000075: [19/Tree_Traversal] getItem(6)
* 00000088: [19/Tree_Traversal] getItem(7)
00000091: [Runner] store final state 0
00000092: [Runner] ---------- example 19/Tree_Traversal is done

Related

I cannot understand WHY I cannot change state in Redux slice

I get the array of objects coming from backend, I get it with socket.io-client. Here we go!
//App.js
import Tickers from "./Components/TickersBoard";
import { actions as tickerActions } from "./slices/tickersSlice.js";
const socket = io.connect("http://localhost:4000");
function App() {
const dispatch = useDispatch();
useEffect(() => {
socket.on("connect", () => {
socket.emit("start");
socket.on("ticker", (quotes) => {
dispatch(tickerActions.setTickers(quotes));
});
});
}, [dispatch]);
After dispatching this array goes to Action called setTickers in the slice.
//slice.js
const tickersAdapter = createEntityAdapter();
const initialState = tickersAdapter.getInitialState();
const tickersSlice = createSlice({
name: "tickers",
initialState,
reducers: {
setTickers(state, { payload }) {
payload.forEach((ticker) => {
const tickerName = ticker.ticker;
const {
price,
exchange,
change,
change_percent,
dividend,
yeild,
last_trade_time,
} = ticker;
state.ids.push(tickerName);
const setStatus = () => {
if (ticker.yeild > state.entities[tickerName].yeild) {
return "rising";
} else if (ticker.yeild < state.entities[tickerName].yeild) {
return "falling";
} else return "noChange";
};
state.entities[tickerName] = {
status: setStatus(),
price,
exchange,
change,
change_percent,
dividend,
yeild,
last_trade_time,
};
return state;
});
return state;
},
},
});
But the state doesn't change. I tried to log state at the beginning, it's empty. After that I tried to log payload - it's ok, information is coming to action. I tried even to do so:
setTickers(state, { payload }) {
state = "debag";
console.log(state);
and I get such a stack of logs in console:
debug
debug
debug
3 debug
2 debug
and so on.

Basic redux-saga, getting back undefined data

So I'm currently learning Redux-Saga and need a little help.
I've received the action and the watcherSaga has caught it and sent it to the workerSaga which runs a function with axios.get to receive data. In the function, I can actually console.log the data and return it, however when it gets back to the saga, the data is undefined. Here are some screenshots, please let me know if you need any other information.
You need to return await axios.get(API_URL).
E.g.
rootSaga.js:
import { call, put, takeEvery } from 'redux-saga/effects';
import { getBlogsSaga } from './getBlogSaga';
const BLOGS = {
LOAD: 'BLOGS_LOAD',
};
function setBlogs(payload) {
return {
type: 'SET_BLOGS',
payload,
};
}
function* displayBlogs() {
const data = yield call(getBlogsSaga);
console.log(data);
yield put(setBlogs(data));
}
function* rootSaga() {
yield takeEvery(BLOGS.LOAD, displayBlogs);
}
export { rootSaga, displayBlogs };
getBlogSaga.ts:
const getBlogsSaga = async () => {
return await Promise.resolve().then(() => {
return [1, 2, 3];
});
};
export { getBlogsSaga };
rootSaga.test.ts:
import { displayBlogs } from './rootSaga';
import { runSaga } from 'redux-saga';
describe('63000691', () => {
it('should pass', async () => {
const dispatched: any[] = [];
await runSaga(
{
dispatch: (action) => dispatched.push(action),
getState: () => ({}),
},
displayBlogs,
).toPromise();
});
});
test result:
PASS src/stackoverflow/63000691/rootSaga.test.ts
63000691
✓ should pass (16 ms)
console.log
[ 1, 2, 3 ]
at src/stackoverflow/63000691/rootSaga.ts:17:11
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.235 s, estimated 3 s
Your arrow function uses curly braces { so there is no implicit return. Either explicitly return axios.get (and incidentally since you are returning a promise there is no need to use async/await) or change to parens to take advantage of the explicit return.
const getBlogsSaga = async () => {
return await axios.get(..
}
or
const getBlogsSaga = async () => (
await axios.get(...
)

Correct type for spawn when running flow-typed?

I'm running a project using Flowtype v0.75 and redux-saga
Currently I'm experiencing some issues trying to determine how to provide the right types for my sagas, specially when I'm trying to combine them:
ActivateAccountSaga.js
// #flow
import Alert from 'react-s-alert';
import { combineSagas, handleError } from 'util/Saga';
import { call, put, take, all } from 'redux-saga/effects';
import { history } from 'modules/LocationModule';
import {
activateAccount,
sendActivationEmail,
sendActivationEmailPending,
sendActivationEmailFulfilled,
sendActivationEmailRejected,
} from 'bundles/Auth/modules/ActivateAccountModule';
import AuthAPI from 'bundles/Auth/apis/AuthAPI';
import config from 'config/index';
export function* activateAccountWorker(api: AuthAPI): Generator<*, *, *> {
while (true) {
const { payload } = yield take(activateAccount().type);
try {
const response = yield call([api, api.activateAccount], payload);
yield call(history.push, config.route.identity.signIn);
yield call(Alert.success, response.description);
} catch (e) {
yield call(history.push, config.route.identity.signIn);
yield call(handleError, e);
}
}
}
export function* sendActivationEmailWorker(api: AuthAPI): Generator<*, *, *> {
while (true) {
const { payload } = yield take(sendActivationEmail().type);
try {
yield put(sendActivationEmailPending());
const response = yield call([api, api.sendActivationMail], payload);
yield put(sendActivationEmailFulfilled(response));
yield call(Alert.success, response.description, { timeout: 30000 });
yield call(history.push, config.route.identity.signIn);
} catch (e) {
yield put(sendActivationEmailRejected(e));
yield call(handleError, e);
}
}
}
export function* activateAccountSaga(api: AuthAPI): Generator<*, *, *> {
yield all(combineSagas([
[activateAccountWorker, api],
[sendActivationEmailWorker, api],
]));
}
const api = new AuthAPI();
export default [activateAccountSaga, api];
And this is the util/Saga.js
// #flow
import get from 'lodash/get';
import Alert from 'react-s-alert';
import { actions } from 'react-redux-form';
import { call, put, spawn, all, select } from 'redux-saga/effects';
export const refineSaga = (saga: * | Array<*>) => {
// Saga is a generator function without params
if (typeof saga === 'function') {
return [saga];
}
// Ensures that a saga in the form [saga, params...] was given
if (Array.isArray(saga) && typeof saga[0] === 'function') {
return saga;
}
throw new TypeError(`Unexpected saga type: ${saga.toString()}`);
};
export const combineSagas = (sagas: Array<Function | *>): any[] => sagas.map(saga => spawn(...refineSaga(saga)));
When running flow v0.75 I get the following error:
Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/bundles/Auth/sagas/ActivateAccountSaga.js:48:6
generator function [1] requires another argument from function type [2] in type argument Fn.
src/bundles/Auth/sagas/ActivateAccountSaga.js
[1] 16│ export function* activateAccountWorker(api: AuthAPI): Generator<*, *, *> {
:
45│
46│ export function* activateAccountSaga(api: AuthAPI): Generator<*, *, *> {
47│ yield all(combineSagas([
48│ [activateAccountWorker, api],
49│ [sendActivationEmailWorker, api],
50│ ]));
51│ }
flow-typed/npm/redux-saga_v0.16.x.js
[2] 863│ <R, Fn: () => R>(fn: Fn): SpawnEffect<null, Fn, []>,
Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/bundles/Auth/sagas/ActivateAccountSaga.js:48:29
A callable signature is missing in AuthAPI [1] but exists in function type [2] in type argument Fn.
src/bundles/Auth/sagas/ActivateAccountSaga.js
45│
[1] 46│ export function* activateAccountSaga(api: AuthAPI): Generator<*, *, *> {
47│ yield all(combineSagas([
48│ [activateAccountWorker, api],
49│ [sendActivationEmailWorker, api],
50│ ]));
51│ }
flow-typed/npm/redux-saga_v0.16.x.js
[2] 863│ <R, Fn: () => R>(fn: Fn): SpawnEffect<null, Fn, []>,
As you can see flow tells me that I should provide another argument, but I'm doing it in an array based fashion, what is the alternative so flow is happy with it?
The issue was in here:
export const refineSaga = (saga: * | Array<*>): Array<*> =>
and
export const combineSagas = (sagas: any[]): any[] => sagas.map(saga => spawn(...refineSaga(saga)));

Redux combineReducer returns default state for reducer not called in action

I'm new to react redux, so I think I'm just missing something basic.
I have three reducers, two to handle orders that update in the store as arrays, and one that shows the status of a web socket connection I'm using to receive orders from the server.
// reducers.js
import { combineReducers } from 'redux'
import { ADD_POS_ORDER, ADD_MOBILE_ORDER, UPDATE_WS_STATUS, wsStatuses } from '../actions/actions'
const { UNINITIALIZED } = wsStatuses
const posOrders = (state = [], action) => {
switch (action.type) {
case ADD_POS_ORDER:
return [
...state,
{
id: action.order.id,
status: action.order.status,
name: action.order.name,
pickupNum: action.order.pickupNum
}
]
default:
return state
}
}
const mobileOrders = (state = [], action) => {
switch (action.type) {
case ADD_MOBILE_ORDER:
return [
...state,
{
id: action.order.id,
status: action.order.status,
name: action.order.name,
pickupNum: action.order.pickupNum
}
]
default:
return state
}
}
const wsStatus = (state = UNINITIALIZED, action) => {
switch (action.type) {
case UPDATE_WS_STATUS:
return action.status
default:
return state
}
}
const displayApp = combineReducers({
posOrders,
mobileOrders,
wsStatus
})
export default displayApp
When I connect to the socket, I dispatch an action to update wsStatus and the action is stored as 'CONNECTED'.
When I follow with an order with the posOrders reducer, the wsStatus is reset to its default, 'UNINITIALIZED'.
What I am struggling to understand is why wsStatus is not using the previous state of 'CONNECTED', but instead returning default.
// actions.js
export const UPDATE_WS_STATUS = 'UPDATE_WS_STATUS'
export const wsStatuses = {
UNINITIALIZED: 'UNINITIALIZED',
CONNECTING: 'CONNECTING',
CONNECTED: 'CONNECTED',
DISCONNECTED: 'DISCONNECTED'
}
export const ADD_POS_ORDER = 'ADD_POS_ORDER'
export const ADD_MOBILE_ORDER = 'ADD_MOBILE_ORDER'
export const UPDATE_POS_ORDER = 'UPDATE_POS_ORDER'
export const setWsStatus = (status) => {
return {
type: 'UPDATE_WS_STATUS',
status: status
}
}
export const updateOrderQueue = (action, order) => {
return {
type: action,
id: order.id,
order: order,
receivedAt: Date.now()
}
}
Here's where I make the calls:
// socketListeners.js
import { setWsStatus } from '../actions/actions'
import SockJS from 'sockjs-client'
export const socket = new SockJS('http://localhost:3000/echo')
export default function (dispatch, setState) {
socket.onopen = function () {
dispatch(setWsStatus('CONNECTED'))
}
socket.onclose = function () {
dispatch(setWsStatus('DISCONNECTED'))
}
}
// orders container
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { socket } from '../helpers/socketListeners'
import { updateOrderQueue, setWsStatus } from '../actions/actions'
import PosOrder from '../components/queue/PosOrder'
class PosOrderList extends Component {
constructor(props) {
super(props)
}
componentDidMount() {
const { dispatch } = this.props
socket.onmessage = function(e) {
// convert order info to object
let parsedOrder = JSON.parse(e.data)
let action = parsedOrder.action
let order = parsedOrder.order
dispatch(updateOrderQueue(action, order))
}
}
render() {
const { updateOrderQueue } = this.props
return (
<ul>
{this.props.posOrders.map(posOrder =>
<PosOrder
key={posOrder.id}
{...posOrder}
/>
)}
</ul>
)
}
}
PosOrderList.propTypes = {
posOrders: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.hash,
status: PropTypes.string,
name: PropTypes.string,
pickupNum: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
}))
}
// send data to component props
const mapStateToProps = (state) => {
return {
posOrders: state.posOrders,
}
}
export default connect(mapStateToProps)(PosOrderList)
// store
const store = configureStore(initialState)
export default function configureStore(initialState) {
return createStore(
displayApp,
initialState,
applyMiddleware(
createLogger({
stateTransformer: state => state.toJS()
}),
thunk,
// socketMiddleware
)
)
}
addSocketListeners(store.dispatch, store.getState)
Lastly, the store logs here: redux store
Any and all help on this would be very appreciated! Thank you!
When you compose your reducer with combineReducers, for each dispatched action, all subreducers get invoked, since every reducer gets a chance to respond to every action.
Therefore, all state gets initialized after the first action is dispatched.
Your reducers are working fine https://jsfiddle.net/on8v2z8j/1/
var store = Redux.createStore(displayApp);
store.subscribe(render);
store.dispatch({type: 'UPDATE_WS_STATUS',status:'CONNECTED'});
store.dispatch({type: 'ADD_POS_ORDER',id:'id'});
store.dispatch({type: 'UPDATE_WS_STATUS',status:'DISCONNECTED'});

How to #connect to async loaded data?

I'm writing an app with React, Redux, react-redux, react-router, react-router-redux and redux-async-connect. The special part of the app is that all client-server communication is done over websockets.
My users can read/write several walls, that I store is a walls store with the following reducer and basic helper functions:
const LOAD = 'ws/wall/LOAD';
const LOAD_SUCCESS = 'estisia/wall/LOAD_SUCCESS';
const LOAD_FAIL = 'estisia/wall/LOAD_FAIL';
const ADD_MESSAGE = 'estisia/wall/ADD_MESSAGE';
const initialWallState = {
loaded: false,
messages: []
};
const initialState = {
walls: {}
};
function wall(state = initialWallState, action = {}) {
switch (action.type) {
... // omitted for brevity
default:
return state;
}
}
export default function walls(state = initialState, action = {}) {
if (typeof action.data === 'undefined' || typeof action.data.wallId === 'undefined') return state;
const newState = {
...state.walls,
[action.data.wallId]: wall(state.walls[action.data.wallId], action)
};
return {walls: newState};
}
export function isLoaded(wallId, globalState) {
return globalState.walls.wallId && globalState.walls.wallId.loaded;
}
export function load(wallId) {
return {
type: LOAD,
send: ['wall/messages/page', [wallId, 1]]
};
}
and I have a Wall container where the appropriate wallId is passed in by react-router, but can't figure out how to make the #connect call pass only the relevant wall data to the container. What should I do instead of the #connect meta-code below to get it working?
#asyncConnect([{
deferred: true,
promise: ({store: {dispatch, getState}, params}) => {
if (!isLoaded(params.wallId, getState())) {
return dispatch(loadWall(params.wallId));
}
}
}])
#connect(
state => ({
messages: state.wall[this.params.wallId].messages,
loading: state.wall[this.params.wallId].loading
}))
export default class Wall extends Component {
static propTypes = {
messages: PropTypes.array
}
... // omitted for brevity
}
How can I achieve redux-async-connect to help me out with the above case?

Resources