I have written render tests for component. I have used action in useEffect that is calling api by triggering saga.
Tests are giving positive result but I am getting following fetch error in from try catch block of saga(calling api)
FetchError {
message: 'invalid json response body at reason: Unexpected end of JSON input',
type: 'invalid-json'
}
What should i do to remove this warning?
My component's useEffect code
const FileUploadContainer = ({
....// some props
}: Props) => {
useEffect(() => {
// action method that calling api by saga
loadOrgUnitClientFiles({
entity: 'xyzzzzz',
query: 'fooooo'
})
}, [loadOrgUnitClientFiles])
return (
<Page title={title}>
.....
</Page>
)
}
export default connect(
(state: RootState) => {
return {
...//mapStateToProps obj
}
},
{
uploadClientFile,
loadOrgUnitClientFiles
}
)(FileUploadContainer)
And the test file is
import React from 'react'
import { render, screen } from 'test-utils/customRenderer'
import FileUploadContainer from './FileUploadContainer'
import { fromJS } from 'immutable'
import SessionRecord from 'shared/reducers/sessionReducer/SessionRecord'
const initialState = fromJS({
...//mock obj
})
describe('FileUploadContainer', () => {
it('renders correctly', () => {
render(<FileUploadContainer />, {
state: initialState
})
expect(screen.getByText('Upload a file for TealBook')).toBeInTheDocument()
expect(screen.getByText('file1.xlsx')).toBeInTheDocument()
})
})
Related
I want to write unit test which checks if data (onLoad) from dispatching async thunk is delivered into state.
It's first time when i'm writing unit tests and it's black magic for me.
My solution it's not working because my state is always empty object.
My component has following logic:
useEffect(() => {
dispatch(getProducts({ filters: false }));
}, [dispatch, filters]);
Here is what i've tried:
import {
render as rtlRender,
screen,
fireEvent,
cleanup,
} from "#testing-library/react";
import { Provider } from "react-redux";
import { store as myStore } from "store/root-reducer";
import ProductList from "components/product/product-list/product-list";
import { productSlice } from "store/slices/product/product.slice";
describe("Product components", () => {
const renderWithRedux = (component) =>
rtlRender(<Provider store={myStore}>{component}</Provider>);
const thunk =
({ dispatch, getState }) =>
(next) =>
(action) => {
if (typeof action === "function") {
return action(dispatch, getState);
}
return next(action);
};
const create = () => {
const store = {
getState: jest.fn(() => ({})),
dispatch: jest.fn(),
};
const next = jest.fn();
const invoke = (action) => thunk(store)(next)(action);
return { store, next, invoke };
};
const initialState = {
product: null,
products: [],
errorMessage: "",
isFetching: false,
filters: false,
};
it("should render product list after dispatching", async () => {
renderWithRedux(<ProductList />);
const { store, invoke } = create();
invoke((dispatch, getState) => {
dispatch("getProducts"); // i want to dispatch asyncthunk which is called getProducts()
getState();
});
expect(store.dispatch).toHaveBeenCalledWith("getProducts");
expect(store.getState).toHaveBeenCalled();
});
});
Can you include your error message?
I think you're almost there. Since you're rendering an actual component and redux store, you're actually doing an integration-style test. My guess is that your dispatch to getProducts() is firing correctly, but the state isn't updating in your test because you haven't mocked an API response. See how msw is being used to do this in the Redux Testing doc
rootReducer
import { combineReducers } from "redux";
import mods from "./mods.js";
export default combineReducers({
mods
})
reducers/mods.js
import { GET_MODS, GET_SPECIFC_MOD } from "../actions/types"
const initialState = {
mods: [],
currMod: []
}
export default function(state = initialState, action) {
switch(action.type) {
case GET_MODS:
return {
...state,
mods: action.payload
}
case GET_SPECIFC_MOD:
return {
...state,
currMod: action.payload
}
default:
return state
}
}
actions/mods.js
import axios from 'axios'
import { GET_MODS, GET_SPECIFC_MOD } from './types'
// get the mods
export const getMods = () => dispatch => {
axios.get('http://localhost:8000/api/mods')
.then(res => {
dispatch({
type: GET_MODS,
payload: res.data
})
}).catch(err => console.log(err))
}
// get single mod
export const getSpecificMod = (title) => dispatch => {
axios.get(`http://localhost:8000/api/mods/${title}`)
.then(res => {
dispatch({
type: GET_SPECIFC_MOD,
payload: res.data
})
}).catch(err => console.log(err))
}
components/download.js
import React from 'react'
import { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { getSpecificMod } from '../actions/mods'
const Download = () => {
useEffect(() => {
const title = window.location.pathname.split('/')[3]
getSpecificMod(title)
})
return (
<></>
)
}
const mapStateToProp = state => ({
currMod: state.mods.currMod
})
export default connect(mapStateToProp, getSpecificMod)(Download)
Response from backend
GET http://localhost:8000/api/mods/function(){return!window.__REDUX_DEVTOOLS_EXTENSION_LOCKED__&&a.dispatch.apply(a,arguments)}
Basically the user clicks on a mod and gets sent to the download section that is handled by 'download.js' the component ('download.js') renders it and reads the window.location to retrieve the title, with redux I want to get the mod so i made a function that takes the title and sends the request 'getMod(title)' but for some reason it is throwing horrible errors that I dont understand, any help is appreciated!
You are not dispatching the action properly in your component. Right now you are actually just calling the getSpecificMod action creator function from your imports. Your Download component doesn't read anything from props so it is ignoring everything that gets created by the connect HOC.
If you want to keep using connect, you can fix it like this:
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import { getSpecificMod } from '../actions/mods'
const Download = ({currMod, getSpecificMod}) => {
const title = window.location.pathname.split('/')[3]
useEffect(() => {
getSpecificMod(title)
}, [title])
return (
<></>
)
}
const mapStateToProps = state => ({
currMod: state.mods.currMod
})
export default connect(mapStateToProps, {getSpecificMod})(Download)
We are now accessing the bound action creator as a prop of the component. mapDispatchToProps is an object which maps the property key to the action.
But it's better to use the useDispatch hook:
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getSpecificMod } from '../actions/mods'
const Download = () => {
const currentMod = useSelector(state => state.mods.currMod);
const dispatch = useDispatch();
const title = window.location.pathname.split('/')[3]
useEffect(() => {
dispatch(getSpecificMod(title));
}, [title, dispatch]);
return (
<></>
)
}
export default Download;
There might be some confusion on terminology here. Your getSpecificMod function is a function which takes dispatch as an argument but it is not a mapDispatchToProps. It is a thunk action creator.
Make sure that you have redux-thunk middleware installed in order to handle this type of action. Or better yet, use redux-toolkit.
Your useEffect hook needs some sort of dependency so that it knows when to run. If you only want it to run once you can use an empty array [] as your dependencies. If you don't specify the dependencies at all then it will re-run on every render.
Does the pathname change? If so, how do you know when? You might want to add an event listener on the window object. Or consider using something like react-router. But that is a separate question.
I have a react-native, redux app, and after upgrading I've started getting some warnings about lifecycle hooks. My code looks like below:
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { selectPosts} from '../redux/selectors/postSelector';
import { getPosts } from '../redux/actions/postActions';
class BasicScreen extends React.Component {
state = {
data: [],
myItems: [],
};
componentWillMount() {
this.getPosts();
}
componentDidMount() {
this.checkforItems();
}
getPosts = async () => {
// Call to a redux action
await this.props.getPosts();
};
checkforItems = async () => {
// myItems in initial state are set from data in
AsyncStorage.getItem('MyItems').then(item => {
if (item) {
this.setState({
myItems: JSON.parse(item),
});
} else {
console.log('No data.');
}
});
};
componentWillReceiveProps(nextProps) {
// Data comes from the redux action.
if (
nextProps.data &&
!this.state.data.length &&
nextProps.data.length !== 0
) {
this.setState({
data: nextProps.data,
});
}
}
render() {
return (
<View>/* A detailed view */</View>
)
}
}
const mapStateToProps = createStructuredSelector({
data: selectPosts,
});
const mapDispatchToProps = dispatch => ({
dispatch,
getPosts: () => dispatch(getPosts()),
});
export default connect(mapStateToProps, mapDispatchToProps)(BasicScreen);
To summarize, I was calling a redux action (this.getPosts()) from componentWillMount(), and then updating the state by props received in componentWillReceiveProps. Now both these are deprecated, and I am getting warnings that these are deprecated.
Apart from this, I am also setting some initial state by pulling some data from storage (this.checkforItems()). This gives me another warning - Cannot update a component from inside the function body of a different component.
To me it looks like the solution lies in converting this into a functional component, however, I'm stuck at how I will call my initial redux action to set the initial state.
UPDATE:
I converted this into a functional component, and the code looks as follows:
import React, { Fragment, useState, useEffect } from 'react';
import { connect } from 'react-redux';
import AsyncStorage from '#react-native-community/async-storage';
import { StyleSheet,
ScrollView,
View,
} from 'react-native';
import {
Text,
Right,
} from 'native-base';
import { createStructuredSelector } from 'reselect';
import {
makeSelectPosts,
} from '../redux/selectors/postSelector';
import { getPosts } from '../redux/actions/postActions';
const BasicScreen = ({ data, getPosts }) => {
const [myData, setData] = useState([]);
const [myItems, setItems] = useState([]);
const checkForItems = () => {
var storageItems = AsyncStorage.getItem("MyItems").then((item) => {
if (item) {
return JSON.parse(item);
}
});
setItems(storageItems);
};
useEffect(() => {
async function getItems() {
await getPosts(); // Redux action to get posts
await checkForItems(); // calling function to get data from storage
setData(data);
}
getItems();
}, [data]);
return (
<View>
<>
<Text>{JSON.stringify(myItems)}</Text>
<Text>{JSON.stringify(myData)}</Text>
</>
</View>
);
}
const mapStateToProps = createStructuredSelector({
data: makeSelectPosts,
});
const mapDispatchToProps = dispatch => ({
dispatch,
getPosts: () => dispatch(getPosts()),
});
export default connect(mapStateToProps, mapDispatchToProps)(BasicScreen);
It works, but the problem is that the first Text - {JSON.stringify(myItems)} - it is rerendering continuously. This data is actually got using checkForItems(). I wanted the useEffect to be called again only when the data updates, but instead something else is happening.
Also, I noticed that setData is not being called correctly. The data becomes available through the prop (data), but not from the state (myData). myData just returns empty array.
I have a small app that displays a component that is a list (JobsList) and another component that that contains a text field and submit button (CreateJob). While I am able to populate JobsList with API data (passing through Redux), I am not sure how I should update JobsList with a new API call, once I have successfully posted a new job in CreateJob. This is the code I have so far:
JobsList.js
import React, { Fragment, useEffect } from 'react';
import { connect } from 'react-redux';
import JobCard from './JobCard';
import CreateJob from './CreateJob';
import api from './Api';
import { JOBS_LOADED } from './ActionTypes';
const JobsList = ({ jobs, onLoad }) => {
useEffect(() => {
const fetchJobs = async () => {
try {
const data = await api.Jobs.getAll();
onLoad({ data });
} catch (err) {
console.error(err);
}
};
fetchJobs();
}, [onLoad]);
return (
<Fragment>
<CreateJob />
{teams.map(job => (
<JobCard job={job} key={team.jobId} />
))}
</Fragment>
);
}
const mapStateToProps = state => ({
jobs: state.jobsReducer.teams
});
const mapDispatchToProps = dispatch => ({
onLoad: payload =>
dispatch({ type: JOBS_LOADED, payload }),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsViewer);
CreateJob.js
import React, { useState } from 'react';
import Button from '#material-ui/core/Button';
import TextField from '#material-ui/core/TextField';
import api from './Api';
const CreateJob = () => {
const [state, setState] = React.useState({
jobName: '',
creator: ''
});
const handleInputChange = event => {
setState({
...state,
[event.target.name]: event.target.value
});
// validation stuff
}
const handleSubmit = async e => {
api.Jobs.create({state})
try {
await request;
// Reload the Jobs list so it does an another API request to get all new data
// DO I CALL A DISPATCH HERE?????
} catch (err) {
console.error(err);
}
}
return (
<div>
<TextField
name="jobName"
value={state.jobName || ''}
onChange={handleInputChange}
/>
<Button onClick={handleSubmit}>Create job</Button>
</div>
);
}
export default CreateJob;
JobsReducer.js
import { TEAMS_LOADED } from './ActionTypes';
export default (state = {teams: []}, action) => {
switch (action.type) {
case TEAMS_LOADED:
return {
...state,
teams: action.payload.data,
};
default:
return state;
}
};
In the success result in handleSubmit in CreateJob.js, how do I trigger/dispatch a new API call to update JobsList from CreateJob.js? I'm new to react/redux so apologies for any poor code. Any advice for a learner is greatly appreciated.
The simplified solution to take is wrapper the function for fetching jobs as a variable in the JobsList, and assign it to CreateJob as a prop. Then from the CreateJob, it's up to you to update the job list.
The shortage of this solution is it doesn't leverage redux as more as we can. It's better to create action creator for shared actions(fetch_jobs) in the JobsReducer.js and map these actions as props to the component which need it exactly.
JobsReducer.js
export const fetchJobsAsync = {
return dispatch => {
try {
const data = await api.Jobs.getAll();
dispatch({type: TEAMS_LOADED, payload: {data}})
} catch (err) {
console.error(err);
}
}
}
tips: You must install redux-thunk to enable the async action.
After, you will be able to fire the API to update the jobs(or teams anyway) from any component by dispatching the action instead of calling the API directly.
JobsList.jsx or CreateJob.js
const mapDispatchToProps = dispatch => ({
fetchAll: () => dispatch(fetchJobsAsync())
})
At the end of CreateJob.js, it's totally the same as calling the fetchAll to reload the jobs list like calling other regular functions.
And, if you are ok to go further, move the API call which creates new job to the reducer and wrapper it as an action. Inside it , dispatching the fetchJobsAsync if the expected conditions meet(If create new job finished successfully). Then you will end up with a more clearly component tree with only sync props without the data logic regarding to when/how to reload the jobs list.
Yes, your approach is absolutely right.
Once you have posted a new job, based on it's response you can trigger fetchJobs which you can pass as prop to <CreateJob fetchJobs={fetchJobs}/>.
For that you will have to declare it outside useEffect() like this:
import React, { Fragment, useEffect } from 'react';
import { connect } from 'react-redux';
import JobCard from './JobCard';
import CreateJob from './CreateJob';
import api from './Api';
import { JOBS_LOADED } from './ActionTypes';
const JobsList = ({ jobs, onLoad }) => {
const fetchJobs = async () => {
try {
const data = await api.Jobs.getAll();
onLoad({ data });
} catch (err) {
console.error(err);
}
};
useEffect(() => {
fetchJobs();
}, [onLoad]);
return (
<Fragment>
<CreateJob fetchJobs={fetchJobs}/>
{teams.map(job => (
<JobCard job={job} key={team.jobId} />
))}
</Fragment>
);
}
const mapStateToProps = state => ({
jobs: state.jobsReducer.teams
});
const mapDispatchToProps = dispatch => ({
onLoad: payload =>
dispatch({ type: JOBS_LOADED, payload }),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsViewer);
Once you trigger the api call new data will be loaded in redux state:
import React, { useState } from 'react';
import Button from '#material-ui/core/Button';
import TextField from '#material-ui/core/TextField';
import api from './Api';
const CreateJob = props => {
const [state, setState] = React.useState({
jobName: '',
creator: ''
});
const handleInputChange = event => {
setState({
...state,
[event.target.name]: event.target.value
});
// validation stuff
}
const handleSubmit = async e => {
api.Jobs.create({state})
try {
await request;
props.fetchJobs()
} catch (err) {
console.error(err);
}
}
return (
<div>
<TextField
name="jobName"
value={state.jobName || ''}
onChange={handleInputChange}
/>
<Button onClick={handleSubmit}>Create job</Button>
</div>
);
}
export default CreateJob;
As JobsList component is subscribed to the state and accepts state.jobsReducer.teams as props here:
const mapStateToProps = state => ({
jobs: state.jobsReducer.teams
});
The props will change on loading new jobs from <CreateJobs />and this change in props will cause <JobsLists /> to be re-rendered with new props.
I am pretty sure i am returning an object and have used asyn and await on the promise within my action file. but this still keeps returing the error redux.js:205 Uncaught Error: Actions must be plain objects. Use custom middleware for async actions
https://codesandbox.io/s/frosty-nash-wdcjf?fontsize=14
my action file is returning an object
import axios from "axios";
export const LOAD_URL_STATUS = "LOAD_URL_STATUS";
export async function loadUrlStatus(url) {
const request = await axios
.get(url)
.then(response => {
console.log(response.status);
return response.status;
})
.catch(error => {
console.log("Looks like there was a problem: \n", error);
});
console.log(request);
console.log(LOAD_URL_STATUS);
return {
type: LOAD_URL_STATUS,
payload: request
};
}
it fails when calling this action in componenDidMount this.props.loadUrlStatus(url);
component
import React from 'react';
import TrafficLight from '../TrafficLight';
import {connect} from 'react-redux';
import {loadUrlStatus} from "../../actions";
//import {withPolling} from "../Polling";
//import Polling from "../Polling/polling";
import { bindActionCreators } from 'redux';
class TrafficLightContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
redOn: true,
yellowOn: false,
greenOn: false,
}
}
componentDidMount(){
console.log("componentDidMount")
const {pollingAction, duration, url} = this.props
//withPolling(this.props.loadUrlStatus(this.props.url),1)
/*
const {pollingAction, duration, url} = this.props
this.dataPolling = setInterval(
() => {
this.props.loadUrlStatus(url);
},
10000);
*/
this.props.loadUrlStatus(url);
};
componentWillUnmount() {
clearInterval(this.dataPolling);
}
render() {
console.log(this.props)
return (
<TrafficLight
Size={100}
onRedClick={() => this.setState({ redOn: !this.state.redOn })}
onGreenClick={() => this.setState({ greenOn: !this.state.greenOn })}
RedOn={this.state.redOn}
GreenOn={this.state.greenOn}
/>
)
}
}
const mapStateToProps = state => ({
...state
});
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{
loadUrlStatus
},
dispatch
);
};
export default (
connect(mapStateToProps, mapDispatchToProps)(TrafficLightContainer));
index
import React from 'react';
import { render } from 'react-dom'
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import configureStore from './configureStore'
const store = configureStore();
const renderApp = () =>
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./App', renderApp)
}
renderApp();
serviceWorker.unregister();
The problem is that loadUrlStatus is async function, so it returns not object, but Promise, and object inside it promise.
To correct this, modify loadUrlStatus so it return another function. As you already applied thunk middleware during store creation, such function will be called inside redux. (You can see samples of async functions here)
export function loadUrlStatus(url) {
// Immediately return another function, which will accept dispatch as first argument. It will be called inside Redux by thunk middleware
return async function (dispatch) {
const request = await axios
.get(url)
.then(response => {
console.log(response.status);
return response.status;
})
.catch(error => {
console.log("Looks like there was a problem: \n", error);
});
console.log(request);
console.log(LOAD_URL_STATUS);
dispatch ({
type: LOAD_URL_STATUS,
payload: request
});
}
}
If you're using await in an action creator, you'll want to return a function from the action creator. Otherwise, return on object. A library like redux-thunk will help you do just that.
Your action creator would then look like this:
import axios from "axios";
export const LOAD_URL_STATUS = "LOAD_URL_STATUS";
export const loadUrlStatus(url) => async dispatch => {
try {
const response = await axios(url)
dispatch({
type: LOAD_URL_STATUS,
payload: response.status
})
} catch (error) {
// dispatch error
}
}