relay modern: how to mock relay for unit testing - react-native

Am trying to test react relay modern container, but am having this issue.
TypeError: Cannot read property 'environment' of undefined
Here is the test code:
test('render component', () => {
const tree = renderer.create(
<User />,
).toJSON();
expect(tree).toMatchSnapshot();
});

Add the below to __mocks__ folder. Then in the test add jest.mock('react-relay'); to the unit test that needs relay. This will mock out relay and leave just the component to test.
import React from 'react';
import PropTypes from 'prop-types';
const relayMock = jest.genMockFromModule('react-relay');
const relayChildContextTypes = {
relay: PropTypes.object,
};
const relayEnvironment = {
lookup: jest.fn(),
};
const relayContext = {
relay: {
environment: relayEnvironment,
variables: {},
},
};
const relayFragmentProps = {
relay: {
environment: relayEnvironment,
},
};
const relayRefetchProps = {
relay: {
environment: relayEnvironment,
refetch: jest.fn(),
},
};
const relayPaginationProps = {
relay: {
environment: relayEnvironment,
hasMore: jest.fn(),
loadMore: jest.fn(),
isLoading: jest.fn(),
},
};
relayMock.__relayEnvironment = relayEnvironment;
relayMock.__relayFragmentProps = relayFragmentProps;
relayMock.__relayRefetchProps = relayRefetchProps;
relayMock.__relayPaginationProps = relayPaginationProps;
const makeRelayWrapper = (relayProps) => (
(Comp) => {
class HOC extends React.Component {
getChildContext() {
return relayContext;
}
render() {
return <Comp {...this.props} {...relayProps}/>;
}
}
HOC.childContextTypes = relayChildContextTypes;
return HOC;
}
);
relayMock.QueryRenderer = jest.fn(() => React.createElement('div', null, 'Test'));
relayMock.createFragmentContainer = makeRelayWrapper(relayFragmentProps);
relayMock.createRefetchContainer = makeRelayWrapper(relayRefetchProps);
relayMock.createPaginationContainer = makeRelayWrapper(relayPaginationProps);
module.exports = relayMock;

You actually dont need to mock the environment variable at all. What I usually do is add:
export class User
to the classdeclaration of the class that I want to test. (Make sure to keep the export default on your connected version of the same class).
I can then test the component the preferred way by importing the component without the need for relay like so in my test:
import { User } from '../User'
This removes the need for mocking relay and you can pass in the props cleanly to the component.

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>
);
};

How to implement Agora SDK videocall to react-native project

I successfully implemented the agora SDK videocall module with virtual background to my react.js web app, but when I try to implement it to the react-native mobile version I keep getting erros I don't know how to solve. I'm a bit new to react-native so this migth be an easy fix but I can't find it.
Basically, after submitting a form with the uid, channel, role, and token (I have a token service) the videocall component is rendered.
These are my dependencies
"agora-access-token": "^2.0.4",
"agora-react-native-rtm": "^1.5.0",
"agora-extension-virtual-background": "^1.1.1",
"agora-rtc-sdk-ng": "^4.14.0",
"agora-rn-uikit": "^4.0.0",
"axios": "^0.27.2",
"react": "18.0.0",
"react-native": "0.69.4",
"react-native-agora": "^3.7.1",
"react-native-dotenv": "^3.3.1"
This is the main videocall component.
import React,{ useEffect } from "react";
import { useState } from "react";
import axios from "axios";
import { Call } from "./components/Call";
const VideoCallApp = ({ videoCallData }) => {
const [token, setToken] = useState("");
const [virtualBackgroundData, setVirtualBackgroundData] = useState({
type: "img",
// example
// type: 'img',
// value: ''
//
// type: 'blur',
// value: integer // blurring degree, low (1), medium (2), or high (3).
//
// type: 'color',
// value: string // color on hex or string
});
useEffect(() => {
const getToken = async () => {
const url = `${process.env.REACT_APP_AGORA_TOKEN_SERVICE}/rtc/${videoCallData.channel}/${videoCallData.role}/uid/${videoCallData.uid}`;
try {
const response = await axios.get(url);
const token = response.data.rtcToken;
setToken(token);
} catch (err) {
alert(err);
}
};
getToken();
}, []);
return (
token && (
<Call
rtcProps={{
appId: process.env.REACT_APP_AGORA_APP_ID,
channel: videoCallData.channel,
token: token,
uid: videoCallData.uid,
}}
virtualBackground={virtualBackgroundData}
/>
)
);
};
export default VideoCallApp;
Which renders the Call component, it has more functionality for the virtual background.
import { useEffect, useState } from 'react'
import AgoraRTC from 'agora-rtc-sdk-ng';
import VirtualBackgroundExtension from 'agora-extension-virtual-background';
import { LocalVideo } from './LocalVideo';
import { RemoteVideo } from './RemoteVideo';
import { VideoControllers } from './VideoButtons';
import { View } from 'react-native';
const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
const extension = new VirtualBackgroundExtension();
AgoraRTC.registerExtensions([extension]);
export const Call = ({ rtcProps = {}, virtualBackground = {} }) => {
const [localTracks, setLocalTracks] = useState({
audioTrack: null,
videoTrack: null
});
const [processor, setProcessor] = useState(null);
useEffect(() => {
if (processor) {
try {
const initProcessor = async () => {
// Initialize the extension and pass in the URL of the Wasm file
await processor.init(process.env.PUBLIC_URL + "/assets/wasms");
// Inject the extension into the video processing pipeline in the SDK
localTracks.videoTrack.pipe(processor).pipe(localTracks.videoTrack.processorDestination);
playVirtualBackground();
}
initProcessor()
} catch (e) {
console.log("Fail to load WASM resource!"); return null;
}
}
}, [processor]);
useEffect(() => {
if (localTracks.videoTrack && processor) {
setProcessor(null);
}
}, [localTracks]);
const playVirtualBackground = async () => {
try {
switch (virtualBackground.type) {
case 'color':
processor.setOptions({ type: 'color', color: virtualBackground.value });
break;
case 'blur':
processor.setOptions({ type: 'blur', blurDegree: Number(virtualBackground.value) });
break;
case 'img':
const imgElement = document.createElement('img');
imgElement.onload = async () => {
try {
processor.setOptions({ type: 'img', source: imgElement });
await processor.enable();
} catch (error) {
console.log(error)
}
}
imgElement.src = process.env.PUBLIC_URL + '/assets/backgrounds/background-7.jpg';
imgElement.crossOrigin = "anonymous";
break;
default:
break;
}
await processor.enable();
} catch (error) {
console.log(error)
}
}
const join = async () => {
await client.join(rtcProps.appId, rtcProps.channel, rtcProps.token, Number(rtcProps.uid));
}
const startVideo = () => {
AgoraRTC.createCameraVideoTrack()
.then(videoTrack => {
setLocalTracks(tracks => ({
...tracks,
videoTrack
}));
client.publish(videoTrack);
videoTrack.play('local');
})
}
const startAudio = () => {
AgoraRTC.createMicrophoneAudioTrack()
.then(audioTrack => {
setLocalTracks(tracks => ({
...tracks,
audioTrack
}));
client.publish(audioTrack);
});
}
const stopVideo = () => {
localTracks.videoTrack.close();
localTracks.videoTrack.stop();
client.unpublish(localTracks.videoTrack);
}
const stopAudio = () => {
localTracks.audioTrack.close();
localTracks.audioTrack.stop();
client.unpublish(localTracks.audioTrack);
}
const leaveVideoCall = () => {
stopVideo();
stopAudio();
client.leave();
}
async function startOneToOneVideoCall() {
join()
.then(() => {
startVideo();
startAudio();
client.on('user-published', async (user, mediaType) => {
if (client._users.length > 1) {
client.leave();
alert('Please Wait Room is Full');
return;
}
await client.subscribe(user, mediaType);
if (mediaType === 'video') {
const remoteVideoTrack = user.videoTrack;
remoteVideoTrack.play('remote');
}
if (mediaType === 'audio') {
user.audioTrack.play();
}
});
});
}
// Initialization
function setProcessorInstance() {
if (!processor && localTracks.videoTrack) {
// Create a VirtualBackgroundProcessor instance
setProcessor(extension.createProcessor());
}
}
async function setBackground() {
if (localTracks.videoTrack) {
setProcessorInstance()
}
}
useEffect(() => {
startOneToOneVideoCall();
}, []);
return (
<View >
<View>
<LocalVideo />
<RemoteVideo />
<VideoControllers
actions={{
startAudio,
stopAudio,
startVideo,
stopVideo,
leaveVideoCall,
startOneToOneVideoCall,
setBackground
}}
/>
</View>
</View>
)
}
The local an remote video component are emty Views where the videos are displayed and the VideoControllers are Buttons that manage the videocall.
When I run the app the form works fine but as soon as I subbmit it the app crashes with these errors.
WARN `new NativeEventEmitter()` was called with a non-null argument without the required `addListener` method.
WARN `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method.
LOG Running "videocall" with {"rootTag":1}
ERROR TypeError: window.addEventListener is not a function. (In 'window.addEventListener("online", function () {
_this32.networkState = EB.ONLINE;
})', 'window.addEventListener' is undefined)
VideoCallApp
Form#http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=com.videocall&modulesOnly=false&runModule=true:121056:41
RCTView
View
RCTView
View
AppContainer#http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=com.videocall&modulesOnly=false&runModule=true:78626:36
videocall(RootComponent)
ERROR TypeError: undefined is not an object (evaluating '_$$_REQUIRE(_dependencyMap[5], "./components/Call").Call')
Something is happening at the Call component and I think it migth be the DOM manipulation for the videos but I can't find an example of a react-native project with agora SDK.
I don't want to use the UIkit because, eventhough it works, I can't use the virtual background which I need for this project. Can anyone help me?

Reset Vue for every jest test?

I am using Vue JS with #vue/test-utils and jest. For my tests I am calling:
let localVue = createLocalVue();
vueMount(MyComponent, { localVue: localVue, options });
The problem is, I am referencing libraries which does stuff like this:
import Vue from 'vue'
import Msal from 'vue-msal'
//...
Vue.use(Msal, {...});
The Vue.use() registers some global stuff on the prototype, etc. For testing purposes, I need this to start fresh each test. The only thing I could think of is to use mockImplementation() with jest on the Vue object. But I am not quite sure how I could accomplish that, if at all possible.
Is there any way to do this? Thanks!
It took me a while, but here is how I solved this...
let setupComplete = false;
let setupFailure = false;
let testContext = {};
function resetTestContext() {
Object.keys(testContext).forEach(function(key) { delete testContext[key]; });
}
function createTestContext(configureTestContext) {
beforeEach(() => {
jest.isolateModules(() => {
setupFailure = true;
jest.unmock('vue');
resetTestContext();
testContext.vueTestUtils = require('#vue/test-utils');
testContext.vue = testContext.vueTestUtils.createLocalVue();
jest.doMock('vue', () => testContext.vue);
testContext.vuetify = require('vuetify');
testContext.vue.use(testContext.vuetify);
testContext.vuetifyInstance = new testContext.vuetify();
if (configureTestContext) {
configureTestContext(testContext);
}
setupComplete = true;
setupFailure = false;
});
});
afterEach(() => {
setupComplete = false;
resetTestContext();
jest.resetModules();
setupFailure = false;
});
return testContext;
},
What made this possible was the jest.isolateModules() method. With this approach, Vue and it's prototype, and also Vuetify, are completely recreated and brand new with each test case.
For it to work, the test spec and the library containing the utility above may not 'import' Vue or any module which depends on Vue. Instead, it needs to be required in the configureTestContext() function or in the test case itself.
My test specs look like this:
import createTestContext from '#/scripts/createTestContext'
describe('sample', () => {
const testContext = createTestContext(function configureTestContext(testContext)
{
testContext.vueDependency = require('#/scripts/vueDependency').default;
});
test('demo', () => {
// I added a helper to more easily do this in the test context...
const sample = testContext.vueTestUtils.mount(require('#/components/Sample').default, {
localVue: testContext.vue,
vuetify: testContext.vuetifyInstance
});
expect(testContext.vueDependency.doSomething(sample)).toBe(true);
expect(sample.isVueInstance()).toBeTruthy();
});
});
import { shallowMount, createLocalVue } from '#vue/test-utils';
import Vuex from 'vuex';
const localVue = createLocalVue();
const { user } = require('./customer.mock');
const originUser = { ...user };
const resetUserData = wrapper => {
wrapper.setData( { userData: originUser } );
};
const TestComponent = localVue.component( 'TestComponent', {
name : 'TestComponent',
data : () => {
return { userData: user };
},
render( createElement ) {
return createElement( 'h3', 'hoy hoy' );
},
} );
describe( 'computed fields', () => {
afterEach( () => {
resetUserData( wrapper );
} );
it( 'isPrivatePerson should return false', () => {
wrapper.setData( { userData: { Contacts: [{ grpid: 'bad field' }] } } );
expect( !wrapper.vm.isPrivatePerson ).toBeTruthy();
} );
it( 'isPrivatePerson should return true', () => {
expect( wrapper.vm.isPrivatePerson ).toBeTruthy();
} );
});

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

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

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?