redux-observable TypeError: Cannot read property 'type' of undefined - redux-observable

I have been trying to implement react server-side-rendering using next, and i am using the with-redux-observable-app example, the example works fine, but i would like to improve the project a little bit by doing
redux modular pattern
fractal project structure
If possible, i would like to implement stateless components
Because #2, i can no longer use react state lifecycle, to solve that i usually took advantage of react router onEnter props, but this suggest that i should use componentWillMount, which doesn't meet my #2 condition
I have put the project on github, with this particular problem committed on this branch
Here's the summary of what i did so far
to achieve #1
// ./redux/index.js
...
import rootEpics from './root/epics'
import rootReducers from './root/reducers'
export default function initStore(initialState) {
const epicMiddleware = createEpicMiddleware(rootEpics)
const logger = createLogger({ collapsed: true })
const middlewares = applyMiddleware(thunkMiddleware, epicMiddleware, logger)
return createStore(rootReducers, initialState, middlewares)
}
// ./redux/root/epics.js
import { fetchCharacterEpic, startFetchingCharactersEpic } from '../ducks/Character/epics'
const rootEpics = combineEpics(
fetchCharacterEpic,
startFetchingCharactersEpic,
)
export default rootEpics
// ./redux/root/reducers.js
import { combineReducers } from 'redux'
import Character from '../ducks/Character'
const rootReducers = combineReducers({
Character,
})
export default rootReducers
// ./redux/ducks/Character/index.js
import * as types from './types'
const INITIAL_STATE = {
data: {},
error: {},
id: 1,
}
const Character = (state = INITIAL_STATE, { type, payload }) => {
switch (type) {
case types.FETCH_CHARACTER_SUCCESS:
return {
...state,
data: payload.response,
id: state.id + 1,
}
case types.FETCH_CHARACTER_FAILURE:
return {
...state,
error: payload.error,
}
default:
return state
}
}
export default Character
// ./redux/ducks/Character/types.js
export const FETCH_CHARACTER = 'FETCH_CHARACTER'
export const FETCH_CHARACTER_SUCCESS = 'FETCH_CHARACTER_SUCCESS'
export const FETCH_CHARACTER_FAILURE = 'FETCH_CHARACTER_FAILURE'
export const START_FETCHING_CHARACTERS = 'START_FETCHING_CHARACTERS'
export const STOP_FETCHING_CHARACTERS = 'STOP_FETCHING_CHARACTERS'
// ./redux/ducks/Character/actions.js
import * as types from './types'
export const startFetchingCharacters = () => ({
type: types.START_FETCHING_CHARACTERS,
})
export const stopFetchingCharacters = () => ({
type: types.STOP_FETCHING_CHARACTERS,
})
export const fetchCharacter = id => ({
type: types.FETCH_CHARACTER,
payload: { id },
})
export const fetchCharacterSuccess = response => ({
type: types.FETCH_CHARACTER_SUCCESS,
payload: { response },
})
export const fetchCharacterFailure = error => ({
type: types.FETCH_CHARACTER_FAILURE,
payload: { error },
})
// ./redux/ducks/Character/epics.js
import 'rxjs'
import { of } from 'rxjs/observable/of'
import { takeUntil, mergeMap } from 'rxjs/operators'
import { ofType } from 'redux-observable'
import ajax from 'universal-rx-request'
import * as actions from './actions'
import * as types from './types'
export const startFetchingCharactersEpic = action$ => action$.pipe(
ofType(types.START_FETCHING_CHARACTERS),
mergeMap(() => action$.pipe(
mergeMap(() => of(actions.fetchCharacter())),
takeUntil(ofType(types.STOP_FETCHING_CHARACTERS)),
)),
)
export const fetchCharacterEpic = (action$, id) => action$.pipe(
ofType(types.FETCH_CHARACTER),
mergeMap(() => ajax({
url: 'http://localhost:8010/call',
method: 'post',
data: {
method: 'get',
path: `people/${id}`,
},
})
.map(response => actions.fetchCharacterSuccess(
response.body,
true,
))
.catch(error => of(actions.fetchCharacterFailure(
error.response.body,
false,
)))),
)
to achieve #2
// ./pages/index/container/index.js
import React from 'react'
import { connect } from 'react-redux'
import { of } from 'rxjs/observable/of'
import rootEpics from '../../../redux/root/epics'
import { fetchCharacter } from '../../../redux/ducks/Character/actions'
import Index from '../component'
const mapStateToProps = state => ({
id: state.Character.id,
})
const mapDispatchToProps = dispatch => ({
async setInitialCharacter(id) {
const epic = of(fetchCharacter({ id }))
const resultAction = await rootEpics(
epic,
id,
).toPromise()
dispatch(resultAction)
},
})
export default connect(mapStateToProps, mapDispatchToProps)((props) => {
props.setInitialCharacter(props.id)
return (<Index />)
})
// ./pages/index/component/index.js
import React from 'react'
import Link from 'next/link'
import Helmet from 'react-helmet'
import Info from '../container/info'
const Index = () => (
<div>
<Helmet
title="Ini index | Hello next.js!"
meta={[
{ property: 'og:title', content: 'ini index title' },
{ property: 'og:description', content: 'ini index description' },
]}
/>
<h1>Index Page</h1>
<Info />
<br />
<nav>
{/* eslint-disable jsx-a11y/anchor-is-valid */}
<Link href="/other"><a>Navigate to other</a></Link><br />
<Link href="/about"><a>Navigate to about</a></Link>
{/* eslint-enable jsx-a11y/anchor-is-valid */}
</nav>
</div>
)
export default Index
// ./pages/index/container/info.js
import { connect } from 'react-redux'
import Info from '../../../components/Info'
const mapStateToProps = state => ({
data: state.Character.data,
error: state.Character.error,
})
export default connect(mapStateToProps)(Info)
with those above, the fetch works fine, but...
i don't want the fetch to keep running, i want it to run just once onEnter.
As an attempt to achieve that, i wrote an epic called startFetchingCharactersEpic(), and an action called startFetchingCharacters(), and lastly add mergeMap(() => of(actions.stopFetchingCharacters())), at the end of fetchCharacterEpic() pipe arguments, with the following scenario in mind
dispatch actions.startFetchingCharacters(), in container
that will trigger startFetchingCharactersEpic()
that will do so until types.STOP_FETCHING_CHARACTERS
that will dispatch actions.fetchCharacter()
that will trigger fetchCharacterEpic()
that will dispatch actions.stopFetchingCharacters()
that will trigger #3
setInitialCharacter
// ./pages/index/container/index.js
const mapDispatchToProps = dispatch => ({
async setInitialCharacter(id) {
const epic = of(startFetchingCharacters())
const resultAction = await rootEpics(
epic,
id,
).toPromise()
dispatch(resultAction)
},
})
but by doing that i got TypeError: Cannot read property 'type' of undefined, the console doesn't give me enough information than saying that the error is coming from setInitialCharacter
Tried googling the issue, but found nothing related to my problem
UPDATE
I manage to make it work again based on #jayphelps' answer below, which brought me back to some of my original problems, which are
How to fully use stateless component without utilizing react state lifecycle, especially replacing onEnter
How to just call the fetchCharacterEpic just once on page load
but i guess these 2 worth another post, as i realized i am asking too many question on this post

Totally guessing here, but it's possible that the error is coming from the fact that you're dispatching a Promise here:
const resultAction = await rootEpics(
epic,
id,
).toPromise()
dispatch(resultAction)
Your question doesn't mention, but that means you must have middleware that intercepts that promise since redux (and redux-observable) only expected POJOs { type: string }.
It's also possible that the promise isn't resolving to anything other than undefined, in which case the ofType operator in your epics will choke because it only works on those POJO actions { type: string }.
Sorry I can't help more specifically, it's tough to follow what the intent is.
e.g. this await rootEpics(epic, id) seems odd as rootEpics is the root epic and expects the arguments to be (action$, store) and UI components should not directly call epics?

Related

Awaiting asynchronous params when using xstate `useInterpret`

I want to enable persistance for react-native application.
Following tutorial on https://garden.bradwoods.io/notes/javascript/state-management/xstate/global-state#rehydratestate
I can't use asynchronous code inside xstate's hook useInterpret
Original code (which uses localStorage instead of AsyncStorage) doesn't have that issue since localStorage is synchronous.
import AsyncStorage from '#react-native-async-storage/async-storage';
import { createMachine } from 'xstate';
import { createContext } from 'react';
import { InterpreterFrom } from 'xstate';
import { useInterpret } from '#xstate/react';
export const promiseMachine = createMachine({
id: 'promise',
initial: 'pending',
states: {
pending: {
on: {
RESOLVE: { target: 'resolved' },
REJECT: { target: 'rejected' },
},
},
resolved: {},
rejected: {},
},
tsTypes: {} as import('./useGlobalMachine.typegen').Typegen0,
schema: {
events: {} as { type: 'RESOLVE' } | { type: 'REJECT' },
},
predictableActionArguments: true,
});
export const GlobalStateContext = createContext({
promiseService: {} as InterpreterFrom<typeof promiseMachine>,
});
const PERSISTANCE_KEY = 'test_key';
export const GlobalStateProvider = (props) => {
const rehydrateState = async () => {
return (
JSON.parse(await AsyncStorage.getItem(PERSISTANCE_KEY)) ||
(promiseMachine.initialState as unknown as typeof promiseMachine)
);
};
const promiseService = useInterpret(
promiseMachine,
{
state: await rehydrateState(), // ERROR: 'await' expressions are only allowed within async functions and at the top levels of modules.
},
(state) => AsyncStorage.setItem(PERSISTANCE_KEY, JSON.stringify(state))
);
return (
<GlobalStateContext.Provider value={{ promiseService }}>
{props.children}
</GlobalStateContext.Provider>
);
};
I tried to use .then syntax to initialize after execution of async function but it caused issue with conditional rendering of hooks.
I had the same use case recently and from what I found there is no native way for xState to handle the async request. What is usually recommended is to introduce a generic wrapper component that takes the state from the AsyncStorage and pass it a prop to where it is needed.
In your App.tsx you can do something like:
const [promiseMachineState, setPromiseMachineState] = useState<string | null>(null);
useEffect(() => {
async function getPromiseMachineState() {
const state = await AsyncStorage.getItem("test_key");
setPromiseMachineState(state);
}
getAppMachineState();
}, []);
return (
promiseMachineState && (
<AppProvider promiseMachineState={promiseMachineState}>
...
</AppProvider>
)
)
And then in your global context you can just consume the passed state:
export const GlobalStateProvider = (props) => {
const promiseService = useInterpret(
promiseMachine,
{
state: JSON.parse(props.promiseMachineState)
},
(state) => AsyncStorage.setItem(PERSISTANCE_KEY, JSON.stringify(state))
);
return (
<GlobalStateContext.Provider value={{ promiseService }}>
{props.children}
</GlobalStateContext.Provider>
);
};

redux-thunk: actions must be plain objects

I have been trying to use redux and redux-thunk to help get a json file from a api and have been getting a warning stating that action must be a plain object. I am really confused as to where the issue is in the code. i have tried following many other stackoverflow posts and a couple of guides online and have not really got a good grasp of where I am going wrong. I understand that this is a problem with how I am referencing async and dispatch but do not know how to fix it.
This is the function that causes the warning to appear in the simulator
export const fetchData = url => {
console.log("Should enter async dispatch");
return async (dispatch) => {
dispatch(fetchingRequest());
fetch("https://randomuser.me/api/?results=10")
.then(response => {
if (response.ok) {
let json = response.json();
dispatch(fetchingSuccess(json));
console.log("JSON", json);
}
})
.catch(error => {
dispatch(fetchingFailure(error));
console.log("Error", error);
});
};
};
Here is the output in the console
Possible Unhandled Promise Rejection (id: 0):
Error: Actions must be plain objects. Use custom middleware for async actions.
Error: Actions must be plain objects. Use custom middleware for async actions.
Edit: including setup of middleware
I have the middleware setup in the index.js file of my app
index.js
import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";
import { Provider } from "react-redux";
import React, { Components } from "react";
import { createStore, applyMiddleware } from "redux";
import appReducer from "./src/data/redux/reducers/appReducer";
import thunk from "redux-thunk";
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
const store = createStoreWithMiddleware(appReducer);
console.log("Store", store.getState());
const AppContainer = () => (
<Provider store = {store}>
<App />
</Provider>
);
AppRegistry.registerComponent(appName, () => AppContainer);
I learned this implementation of store from a Youtube Tutorial.
Edit 2: Adding in the fetchData call
I call fetchData in a _onPress function like this
_onPress = () => {
const {fetchData} = this.props;
let url = "https://randomuser.me/api/?results=10";
fetchData(url);
console.log("should have fetched");
};
this is how my app has been connected to redux
const mapStateToProps = state => {
return { response: state };
};
const mapStateToDispatch = dispatch => ({
fetchData: url => dispatch(fetchData(url)),
});
export default connect(
mapStateToProps,
mapStateToDispatch
)(SearchScreen);
these are the action in my app
export const fetchingRequest = () => {
{
type: FETCHING_REQUEST;
}
};
export const fetchingSuccess = json => {
{
type: FETCHING_SUCCESS;
payload: json;
}
};
export const fetchingFailure = error => {
{
type: FETCHING_FAILURE;
payload: error;
}
};
I was able to figure out the problem thanks to working through the steps in the comments thanks to Michael Cheng. I ended up finding that the problem was that i had actions with plain objects but they were not returning anything.
The original actions were
export const fetchingRequest = () => {
{
type: FETCHING_REQUEST;
}
};
export const fetchingSuccess = json => {
{
type: FETCHING_SUCCESS;
payload: json;
}
};
export const fetchingFailure = error => {
{
type: FETCHING_FAILURE;
payload: error;
}
};
to this
export const fetchingRequest = () => {
return {
type: FETCHING_REQUEST
}
};
export const fetchingSuccess = json => {
return {
type: FETCHING_SUCCESS,
payload: json
}
};
export const fetchingFailure = error => {
return {
type: FETCHING_FAILURE,
payload: error
};
};
with including the return for each action

vuex store getters not working in a component

Can anyone see why this wouldn't work please,
Trying to use vuex store to manage my axios requests and transfer to a component as follows:
In my vuex store module I have the following
import axios from "axios";
export const state = () => ({
cases: [],
})
export const mutations = {
listCases (state, cases) {
state.cases = cases;
},
}
export const actions = {
loadCases ({ commit, context }) {
return axios.get('http')
.then(res => {
const convertCases = []
for (const key in res.data) {
convertCases.push({ ...res.data[key], id: key })
}
commit('listCases', convertCases)
})
.catch(e => context.error(e));
},
export const getters = {
// return the state
cases(state) {
return state.cases
}
}
I checked amd my axios request is returning my results as expected and passing to the mutation
In my component I have
import { mapMutations, mapGetters, mapActions } from 'vuex'
export default {
created () {
this.$store.dispatch('cases/loadCases');
},
computed: {
...mapGetters ({
cases: 'cases/cases'
})
},
</script>
Now i assumed based on what I've learnt that i could call with
and this would return my items.
but i get an error cases is not defined,
Anyone abe to tell me my error please
Many Thanks
Take a look here: https://v2.vuejs.org/v2/guide/list.html#Array-Change-Detection
You may be able to make it reactive this way:
export const mutations = {
listCases (state, cases) {
state.cases = [];
cases.forEach((c) => {
state.cases.push(c);
});
},
}

Updating Only When There's New Data, React Native App, Redux-Socket IO?

Goal: Make API calls only when we have new data rather whenever user navigates to screen.
Let's say the app I'm working on has 2 screens the user can navigate to. Receiving Text Screen and Sending Screen. Each user can add friends/followers as well. On the Sending Screen, the user can send a text item to the followers of their choice. On the Receiving Text Screen, the follower user will see the text item pop up at the top of the screen.
Currently, user on the Receiving Text Screen can only see the text item when we make an API call called fetchTexts. We make this call whenever user navigates to this screen or whenever user pulls down to refresh.
class TextScreen extends Component {
static propTypes = {
fetchTexts: PropTypes.func,
};
constructor(props) {
super(props);
this.state = {
refreshing: false,
};
}
componentDidMount() {
textsGATracker();
this.props.fetchTexts(this.props.userId);
}
_onRefresh() {
this.setState({ refreshing: true });
// when the screen is pulled down, fetchTexts
this.props.fetchTexts(this.props.userId);
this.setState({ refreshing: false });
}
const mapStateToProps = (state) => {
return {
texts: state.texts_reducer.texts,
userId: state.user_reducer.userId,
textsReceived: state.texts_reducer.textsReceived,
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchTexts: (userId) => { dispatch(fetchTexts(userId)); },
};
};
These are the Redux Actions that occur with fetchTexts :
import axios from 'axios';
export const REQUEST_TEXTS_ITEMS = 'REQUEST_TEXTS_ITEMS';
export const RECEIVE_TEXTS_ITEMS = 'RECEIVE_TEXTS_ITEMS';
export const FAILED_TEXTS_ITEMS = 'FAILED_TEXTS_ITEMS';
axios.defaults.baseURL = 'nope.herokuapp.com';
/* ======= Actions to Request Texts by userId ======= */
export const requestTextsItems = userId => ({
type: REQUEST_TEXTS_ITEMS,
userId,
});
export const receiveRecsItems = json => ({
type: RECEIVE_TEXTS_ITEMS,
texts: json,
});
export const failedtextsItems = error => ({
type: FAILED_TEXTS_ITEMS,
infoMsg: `The API request failed - ${error}`,
});
export const fetchtexts = userId => (dispatch, getState) => {
const AUTH_TOKEN = `Bearer ${getState().auth_reducer.bearerToken}`;
axios.defaults.headers.common.Authorization = AUTH_TOKEN;
dispatch(requesttextsItems(userId));
axios.get(`/sharesTexts/${userId}`)
.then((response) => {
dispatch(receivetextsItems(response.data));
}, (error) => {
dispatch(failedtextsItems(error));
});
};
On the Sending Screen, user selects a text item to share with their followers. The redux action used looks like this:
export const usersCreateshareTextId = (textId, description, sharerUserId, recipientUserId) => (dispatch, getState) => {
recipientUserId);
const AUTH_TOKEN = `Bearer ${getState().auth_reducer.bearerToken}`;
axios.defaults.headers.common.Authorization = AUTH_TOKEN;
dispatch(requestUsersCreateshare(description, sharerUserId, recipientUserId));
return axios.post(`/users/createShare/${textId}`, {
description,
sharerUserId,
recipientUserId,
})
.then((response) => {
response.data);
dispatch(receiveUsersCreateshare(response.data));
}, (error) => {
dispatch(failedUsersCreateshare(error));
});
};
So I looked into websockets, and now Redux Socket IO v.1.4.0 seems to be my best bet. It looks like it's mainly used in the store. My store looks like this:
import { createStore, applyMiddleware, compose } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import reducers from '../redux/reducers';
const middleware = [thunk];
const persistConfig = {
key: 'root',
storage,
stateReconciler: autoMergeLevel2,
blacklist: ['network', 'auth_reducer', 'search_reducer', 'follow_reducer', 'following_reducer',
'followers_reducer'],
};
if (process.env.NODE_ENV === 'development') {
middleware.push(logger);
}
const persistedReducer = persistReducer(persistConfig, reducers);
export const store = createStore(
persistedReducer,
compose(applyMiddleware(...middleware)),
);
export const persistor = persistStore(store);
How would I implement Redux Socket IO with what I have so far?
Thank you thank you thank!

Redux fetch data from api

I am trying to fetch some data from an api using Redux. My code looks like this:
Action:
// Import libraries
import axios from 'axios';
// Import types
import {
GET_ALL_PICKS
} from './types';
export const getAllPicks = ({ token }) => {
const getPicks = (dispatch) => {
axios({
method: 'get',
url: 'http://myapi/',
headers: {
Authorization: `Bearer ${token}`
}
})
.then((response) => {
console.log(response.data); // First log here returns data just fine
dispatch({
type: GET_ALL_PICKS,
payload: response.data
});
})
.catch((error) => {
console.log(error);
});
};
return getPicks;
};
Reducer:
// Import types
import {
GET_ALL_PICKS
} from '../actions/types';
// Set Initial State
const INITIAL_STATE = {
allPicks: {},
loading: false,
error: ''
};
// Make pick reducers
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case GET_ALL_PICKS:
return { ...state, allPicks: action.payload }; // Logging action.payload here returns data just fine
default:
return state;
}
};
Component:
// Import Libraries
import React, { Component } from 'react';
import { Text } from 'react-native';
import { connect } from 'react-redux';
import {
getAllPicks
} from '../actions/picks';
// Make Component
class HomeScreen extends Component {
// Fetch Data
componentWillMount() {
const { token } = this.props;
this.props.getAllPicks({ token });
}
// Test response
componentDidMount() {
console.log(this.props.allPicks); // This log returns empty object, why?!
}
render() {
return (
<Text>Test</Text>
);
}
}
const mapStateToProps = ({ auth, picks }) => {
const { token } = auth;
const { allPicks } = picks;
return {
token,
allPicks
};
};
export default connect(mapStateToProps, { getAllPicks })(HomeScreen);
When I run the app I see the data in the action console.log and if I run a console.log(action.payload) in the reducer I see the data just fine but in component I see an empty array which suggests I'm not hooking up the data in my reducer correctly? Here's a screen shot of the logs:
I have also tried this in my reducer after some Googling:
return Object.assign({}, state, {
allPicks: action.payload
});
but again I got the same result. Can anyone explain to me what I am doing wrong?
You are confusing the component lifecycle and the API lifecycle.
In practice, what's happening is:
componentWillMount
getAllPicks
componentDidMount (at which point, the API didn't return, the picks are empty)
[... wait for the API to return]
then the API returns with the data, but too late
What you need to do then is check for your "picks" state in the render() function, which will be updated each time your state changes (which happens when the API returns), thanks to the connect() function.
You can also check that the picks are updated properly using componentWillUpdate, not componentDidMount which again has nothing to do with the props being updated.