Awaiting asynchronous params when using xstate `useInterpret` - react-native

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

Related

Unable to update state in redux when using #reduxjs/toolkit

I currently started learning redux. My code was working perfectly with core redux, then I tried out #reduxjs/toolkit and now I'm unable to access the function to change the state in the store. Here is my code of reducer.
const seasonEdits = createSlice({
name: "seasons",
initialState: [],
reducers: {
addSeason(state, action) {
state.push(action.payload);
console.log("this here");
},
removeSeason(state, action) {
state.filter((season) => season.id !== action.payload);
},
markComplete(state, action) {
state.map((season) => {
if (season.id == action.payload) season.isWatched = !season.isWatched;
});
},
},
});
export const { addSeason, removeSeason, markComplete } = seasonEdits.actions;
export default seasonEdits.reducer;
and my store.js file
import { configureStore } from "#reduxjs/toolkit";
import seasonReducer from "./reducer";
export default store = configureStore({
reducer: {
seasons: seasonReducer,
},
});
and the add.js file which has add functionality. Calling a handleSubmit function which is creating an object and adding it to an array which is the state in store.
const handleSubmit = async () => {
try {
if (!name || !totalNoSeason) {
return alert("Please add both fields");
}
const seasonToAdd = {
id: shortid.generate(),
name,
totalNoSeason,
isWatched: false,
};
addSeason(seasonToAdd);
navigation.navigate("Home");
} catch (error) {
console.log(error);
}
};
const mapDispatchToProps = (dispatch) => {
return {
addSeason: (data) => dispatch(addSeason(data)),
};
};
Add.propTypes = {
addSeason: propTypes.func.isRequired,
};
export default connect(null, mapDispatchToProps)(Add);
The issue is that array.map() and array.filter() return new arrays! Right now your reducers are calling those functions, and then just throwing away the new arrays:
removeSeason(state, action) {
// The return value is thrown away and ignored!
state.filter((season) => season.id !== action.payload);
},
You need to return the new value:
removeSeason(state, action) {
// Now RTK will see the new return value
return state.filter((season) => season.id !== action.payload);
},
See https://redux-toolkit.js.org/usage/immer-reducers#resetting-and-replacing-state for more details.

Api call happens only the first time in react-native redux

I am new to using react native redux and I am facing an issue that the api call is made only once, what if i click on another button which should render a different response based on the params and display it on the component which is a flatlist in my case. Please have a look at my code.
RecordListAction:
import { FETCH_RECORD_LIST, FETCH_RECORD_SUCCESS, FETCH_RECORD_FAILURE } from './types.js'
export const fetchRecordList = () => ({
type: FETCH_RECORD_LIST
})
export const fetchRecordSuccess = json => ({
type: FETCH_RECORD_SUCCESS,
payload: json
})
export const fetchRecordFailure = error => ({
type: FETCH_RECORD_FAILURE,
payload: error
})
export const fetchRecordListApi = () => {
console.log("Now I'm here!")
return async dispatch => {
dispatch(fetchRecordList());
let response = await
fetch(url, {
method: 'POST',
headers: {
'tenantid': '1',
'Content-Type': 'application/json',
'language': '1',
'userid': '11'
},
body: JSON.stringify(global.recordListBody)
}).then((response) => response.json())
.then((responseJson) => {
console.log("RecordList Action Value" + responseJson)
dispatch(fetchRecordSuccess(responseJson.records));
}).catch(error => {
dispatch(fetchRecordFailure(error))
}) }}
recordListReducer.js:
import {FETCH_RECORD_REQUEST,FETCH_RECORD_SUCCESS,FETCH_RECORD_FAILURE}
from "../actions/types"
const initialState = {
isFetching: false,
errorMessage : '',
record :[]
};
const recordListReducer = (state = initialState,action) => {
switch(action.type){
case FETCH_RECORD_REQUEST:
return { ...state, isFetching: true }
case FETCH_RECORD_FAILURE:
return { ...state, isFetching: false, errorMessage: action.payload };
case FETCH_RECORD_SUCCESS:
return{...state, isFetching:false, record:action.payload}
default:
return state
}};
export default recordListReducer;
RecordListContainer.js
import React, { Component } from 'react'
import { Text, View, StyleSheet, ActivityIndicator, Button } from 'react-native'
import PropTypes from 'prop-types';
import {fetchRecordListApi} from "../redux/actions/recordListAction"
import {connect} from "react-redux";
import DetailsViewMode from '../Enums/DetailsViewMode'
import RecordList from '../Components/RecordListComponents/RecordList';
import { Icon, Divider } from 'react-native-elements';
class RecordListContainer extends Component {
constructor(props) {
super(props);
}
componentDidMount(){
this.props.dispatch(fetchRecordListApi());
}
render(){
let content = <RecordList record = {this.props.recordList.record}/>
if(this.props.recordList.isFetching){
content= <ActivityIndicator size="large" />
}
}}
RecordListContainer.propTypes = {
fetchRecordListApi : PropTypes.func.isRequired,
recordList : PropTypes.object.isRequired}
const mapStateToProps = state =>{
return{
recordList: state.posts
};
}
export default connect(mapStateToProps)(RecordListContainer);
rootReducer.js :
import recordListReducer from './recordListReducers';'
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
posts : recordListReducer,
});
export default rootReducer;
You could make recordListBody part of redux state or react context. Or you could make recordListBody observable and respond to changes. Here is an example of making recordListBody observable:
//object combined with global.recordListBody to add listeners
// and notify them of changes
const recordListBodyObserver = ((observers) => {
const removeObserver = (observer) => () => {
observers = observers.filter((o) => o !== observer);
};
return {
notify: (value) =>
observers.forEach((observer) => observer(value)),
add: (observer) => {
observers.push(observer);
return removeObserver(observer);
},
};
})([]);
let recordListBodyValue;
//your global object with recordListBody that will notify
// listeners if a value for recordListBody is set
const global = {
set recordListBody(value) {
//notify all listeners;
recordListBodyObserver.notify(value);
//set the new value
return (recordListBodyValue = value);
},
get recordListBody() {
return recordListBodyValue;
},
};
//function to create increasing id
const id = ((id) => () => id++)(1);
class App extends React.PureComponent {
componentDidMount() {
this.removeListener = recordListBodyObserver.add(
(value) => {
//you can dispatch your action here using value
// do not use global.recordListBody here becasue
// that has the old valuee
console.log(
'recordListBody changed from:',
global.recordListBody,
'to value:',
value
);
}
);
}
componentWillUnmount() {
//clean up listener when component unmounts
this.removeListener();
}
render() {
return (
<button
onClick={() => (global.recordListBody = id())}
>
Change recordListBody
</button>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I am using componentDidUpdate and check if props value is changed, the api is again called when the body coming in props is changed.

store.getstate() evaluate to undefined

in a react-native project,I keep hitting in this error:
state is undefined, evaluating store.getstate()
//store.js
const composeEnhancers =
typeof window === "object" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
const enhancer = composeEnhancers(applyMiddleware(thunk));
const Store = createStore(
combineReducers(
{
form: formReducer,
appointmentsReducer
},
enhancer
)
);
console.log(Store.getState());
export default Store;`
//reducer.js
import {
FETCH_APPOINTMENTS_BEGIN,
FETCH_APPOINTMENTS_SUCCESS,
FETCH_APPOINTMENTS_FAILURE
} from '../actions/appointmentsAction';
const initialState = {
data: [],
loading: false,
error: null
};
export default function appointmentsReducer(state = initialState, action) {
switch(action.type) {
case FETCH_APPOINTMENTS_BEGIN:
return {
...state,
loading: true,
error: null
};
case FETCH_APPOINTMENTS_SUCCESS:
return {
...state,
loading: false,
data: action.payload.appointments
};
case FETCH_APPOINTMENTS_FAILURE:
return {
...state,
loading: false,
error: action.payload.error,
data: []
};
default:
return state;
}
}
//actions.js
import { listUrl } from "../cst";
export const FETCH_APPOINTMENTS_BEGIN = "FETCH_APPOINTMENTS_BEGIN";
export const FETCH_APPOINTMENTS_SUCCESS = "FETCH_APPOINTMENTS_SUCCESS";
export const FETCH_APPOINTMENTS_FAILURE = "FETCH_PRODUCTS_FAILURE";
export const fetchAppointmentsBegin = () => ({
type: FETCH_APPOINTMENTS_BEGIN
});
export const fetchAppointmentsSuccess = appointments => ({
type: FETCH_APPOINTMENTS_SUCCESS,
payload: { appointments }
});
export const fetchAppointmentsFailure = error => ({
type: FETCH_APPOINTMENTS_FAILURE,
payload: { error }
});
export function fetchAppointments() {
return dispatch => {
dispatch(fetchAppointmentsBegin());
return fetch(listUrl)
.then(handleErrors)
.then(res => res.json())
.then(json => {
dispatch(fetchApointmentsSuccess(json.appointment));
return json.appointment;
})
.catch(error => dispatch(fetchAppointmentsFailure(error)));
};
}
// Handle HTTP errors since fetch won't.
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
// app.js
export default function App() {
return (
<Provider store={Store}>
<Navigation />
</Provider>
);
}
//list rendrer component :
const mapStateToProps = state => ({
data: state.appointments.data,
loading: state.loading,
error: state.error
});
the console.log of store.getstate() gives :
Object {
"appointmentsReducer": Object {
"data": Array [],
"error": null,
"loading": false,
},
"form": Object {},
I'm not sure where the problem is.
Is it due to the asynchronous call not being handled properly?
If I use saga to handle the fetch, will it resolve the problem?
Any help would be appreciated .

relay modern: how to mock relay for unit testing

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.

How to make external api calls in redux react

My code is as below:
const LOAD = 'redux-example/LOAD';
const LOAD_SUCCESS = 'redux-example/LOAD_SUCCESS';
const LOAD_FAIL = 'redux-example/LOAD_FAIL';
import axios from 'axios';
const initialState = {
loaded: false
};
export default function info(state = initialState, action = {}) {
switch (action.type) {
case LOAD:
return {
...state,
loading: true
};
case LOAD_SUCCESS:
return {
...state,
loading: false,
loaded: true,
data: action.result
};
case LOAD_FAIL:
return {
...state,
loading: false,
loaded: false,
error: action.error
};
default:
return state;
}
}
export function load() {
return {
types: [LOAD, LOAD_SUCCESS, LOAD_FAIL],
promise: (client) => client.get('http://example.com/getdata')
};
}
I am using https://github.com/erikras/react-redux-universal-hot-example example as starter kit. I want to make promise based api call to example.com/api.But I am not able to do it with async call.I get error in middleware that can not read promise of undefined.My middleware code is as below.
export default function clientMiddleware(client) {
return ({dispatch, getState}) => {
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
const { promise, types, ...rest } = action; // eslint-disable-line no-redeclare
if (!promise) {
return next(action);
}
const [REQUEST,SUCCESS,FAILURE] = types;
next({...rest, type: REQUEST});
const actionPromise = promise(client);
actionPromise.then(
(result) => next({...rest, result, type: SUCCESS}),
(error) => next({...rest, error, type: FAILURE})
).catch((error)=> {
console.error('MIDDLEWARE ERROR:', error);
next({...rest, error, type: FAILURE});
});
return actionPromise;
};
};
}
MY component code is as below
import React, {Component, PropTypes} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {load} from 'redux/modules/info';
#connect(state => ({info: state.info.data}),dispatch => bindActionCreators({load}, dispatch))
export default class InfoBar extends Component {
static propTypes = {
info: PropTypes.object,
load: PropTypes.func.isRequired
}
render() {
const {info, load} = this.props; // eslint-disable-line no-shadow
const styles = require('./InfoBar.scss');
return (
<div className={styles.infoBar + ' well'}>
<div className="container">
This is an info bar
{' '}
<strong>{info ? info.message : 'no info!'}</strong>
<span className={styles.time}>{info && new Date(info.time).toString()}</span>
<button className="btn btn-primary" onClick={load}>Reload from server</button>
</div>
</div>
);
}
}
this is only the reducer. You would want to create an action. An action triggers the event that will make the redux store update its state. The basic flow of redux for something like this goes like:
Mount a component
Dispatch an action
Dispatched action in turn will update the store via the Provider component
this will trigger a re-render of the component.
The following is a basic example using fetch.
import fetch from 'isomorphic-fetch';
export function getUsers() {
return dispatch => {
dispatch({ type: REQUEST_USERS });
return fetch('/api/v1/users')
.then(res => res.json())
.then(users => {
dispatch({ type: RECEIVE_USERS, payload: users });
return users;
});
}
}
Then you can call this in your component level item.
import { getUsers } from 'actions/users';
class UserList extends Component {
componentDidMount() { dispatch(getUsers()) }
}
Check out the example