i am writing a test case for a component and it is using redux ,saga ,and selectors
The test case as follows
const initialState = {
fetching: false,
error: null,
StockProducts: [],
products: [],
filter: '5b438ae60599132cb8b64b67'
}
const mockStore = configureStore()
const store = mockStore(fromJS(initialState))
import Stock from '../Stock'
test('for onUpdate funcion', () => {
const props = {
setErrorPopUp: jest.fn(),
formSubmitAttempt: jest.fn()
}
const wrapper = shallow(<Stock {...props} store={store} />)
wrapper.instance().onUpdate({ quantity: 0 })
expect(props.setErrorPopUp).toHaveBeenCalled()
wrapper.instance().onUpdate({ quantity: 3 })
expect(props.formSubmitAttempt).toHaveBeenCalled()
})
However test failed and i am getting this error
TypeError: Cannot read property 'fetching' of undefined
12 | createSelector(
13 | selectStockDomain,
> 14 | ({ fetching }) => fetching
15 | )
16 |
17 | const StockError = () =>
That is from one of the selectors i have used for the component
What is the wrong with my test and why is this happening
And how can i resolved this
this is the stock component
// react core
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
// component imports
// Selcetors and actions
import StockActions from './actions'
import ToastActions from 'containers/Toasts/actions'
import {
StockFetching,
StockError,
StockProducts,
StockFilter,
Products,
userData
} from './selectors'
// partials
const mapStateToProps = createStructuredSelector({
fetching: StockFetching(),
error: StockError(),
stockProducts: StockProducts(),
products: Products(),
filter: StockFilter(),
userData: userData()
})
const mapDispatchToProps = dispatch => ({
getStockProducts: () => {
dispatch(StockActions.getStockAttempt())
},
getProducts: payload => {
dispatch(StockActions.getProductsAttempt(payload))
},
formSubmitAttempt: payload => {
dispatch(StockActions.formSubmitAttempt(payload))
},
deleteStock: payload => {
dispatch(StockActions.deleteStockAttempt(payload))
},
setErrorPopUp: payload => {
dispatch(
ToastActions.setToast(payload.message, payload.action, payload.time)
)
},
reset: () => {
dispatch(StockActions.reset())
}
})
class Stock extends Component {
state = {
openCard: false,
showCard: false,
confirmDelete: false
}
// to show product details
onCard = openCard => {
this.setState({
openCard
})
}
// to show and close add to stock option
_ShowCard = () => {
this.setState({
showCard: !this.state.showCard
})
}
// onDlete Stock
onDeleteStock = data => {
let {
props: { deleteStock },
state: { confirmDelete }
} = this
if (confirmDelete) {
deleteStock(data)
this.setState({
confirmDelete: false
})
} else {
this.setState({
confirmDelete: true
})
}
}
// update the products
handlesubmit = data => {
if (parseInt(data.quantity) < 1) {
this.props.setErrorPopUp({
message:
'You cannot set quantity to zero.Please enter valid qunatity and try again',
action: 'danger',
time: '5000'
})
} else {
this.props.formSubmitAttempt({
product: data.product.key,
quantity: data.quantity
})
this._ShowCard()
}
}
onUpdate = data => {
if (parseInt(data.quantity) < 1) {
this.props.setErrorPopUp({
message: 'You cannot set quantity to zero.Use delete actions',
action: 'danger',
time: '5000'
})
} else {
this.props.formSubmitAttempt({
product: data.id,
quantity: data.quantity
})
}
}
onCancelDelete = () => {
this.setState({
confirmDelete: false
})
}
onGetStock = () => {
let { getStockProducts, getProducts, userData } = this.props
getStockProducts()
getProducts({ user: userData._id })
}
componentDidMount () {
this.onGetStock()
}
render () {
let {
props: { stockProducts, error, products, fetching },
state: { openCard, showCard, confirmDelete },
onCard,
onGetStock,
_ShowCard,
handlesubmit,
onUpdate,
onDeleteStock,
onCancelDelete
} = this
return (
<KeyboardAvoidingWrapper fluid enabled={!showCard}>
UI GOES HERE ....
</KeyboardAvoidingWrapper>
)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Stock)
Thanks
When you are testing a Component, it's a good idea to test just the behaviour of the Component independent of your redux store. Because, internally the store passes in state to the Component as props.
1. export your Component
So your Component definition code becomes
class Stock extends Component {
to
export class Stock extends Component {
2. import just the Component without the Connect HOC
Like this
import { Stock } from '../Stock'
test('for onUpdate funcion', () => {
const props = {
setErrorPopUp: jest.fn(),
formSubmitAttempt: jest.fn()
}
const wrapper = shallow(<Stock {...props} />)
// modify your code to see how your component behaves with different props
wrapper.instance().onUpdate({ quantity: 0 })
expect(props.setErrorPopUp).toHaveBeenCalled()
wrapper.instance().onUpdate({ quantity: 3 })
expect(props.formSubmitAttempt).toHaveBeenCalled()
})
Related
hope someone can point me the right direction with this. Basically I've created a react app which makes use of hooks, specifically useContext, useEffect and useReducer. My problem is that I can't seem to get tests to detect click or dispatch events of the related component.
Stripped down version of my app can be found at : https://github.com/atlantisstorm/hooks-testing
Tests relate to layout.test.js script.
I've tried various approaches, different ways of mocking dispatch, useContext, etc but no joy with it. Most recent version.
layout.test.js
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import Layout from './layout';
import App from './app';
import { Provider, initialState } from './context';
const dispatch = jest.fn();
const spy = jest
.spyOn(React, 'useContext')
.mockImplementation(() => ({
state: initialState,
dispatch: dispatch
}));
describe('Layout component', () => {
it('starts with a count of 0', () => {
const { getByTestId } = render(
<App>
<Provider>
<Layout />
</Provider>
</App>
);
expect(dispatch).toHaveBeenCalledTimes(1);
const refreshButton = getByTestId('fetch-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(3);
});
});
layout.jsx
import React, { useContext, useEffect } from 'react';
import { Context } from "./context";
const Layout = () => {
const { state, dispatch } = useContext(Context);
const { displayThings, things } = state;
const onClickDisplay = (event) => {
// eslint-disable-next-line
event.preventDefault;
dispatch({ type: "DISPLAY_THINGS" });
};
useEffect(() => {
dispatch({ type: "FETCH_THINGS" });
}, [displayThings]);
const btnText = displayThings ? "hide things" : "display things";
return (
<div>
<button data-testid="fetch-button" onClick={onClickDisplay}>{btnText}</button>
{ displayThings ?
<p>We got some things!</p>
:
<p>No things to show!</p>
}
{ displayThings && things.map((thing) =>
<p>{ thing }</p>
)}
</div>
)
}
export default Layout;
app.jsx
import React from 'react';
import Provider from "./context";
import Layout from './layout';
const App = () => {
return (
<Provider>
<Layout />
</Provider>
)
}
export default App;
context.jsx
import React, { createContext, useReducer } from "react";
import { reducer } from "./reducer";
export const Context = createContext();
export const initialState = {
displayThings: false,
things: []
};
export const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
);
};
export default Provider;
reducer.jsx
export const reducer = (state, action) => {
switch (action.type) {
case "DISPLAY_THINGS": {
const displayThings = state.displayThings ? false : true;
return { ...state, displayThings };
}
case "FETCH_THINGS": {
const things = state.displayThings ? [
"thing one",
"thing two"
] : [];
return { ...state, things };
}
default: {
return state;
}
}
};
I'm sure the answer will be easy when I see it, but just trying to figure out I can detect the click event plus detect the 'dispatch' events? (I've already got separate test in the main app to properly test dispatch response/actions)
Thank you in advance.
EDIT
Ok, I think I've got a reasonable, though not perfect, solution. First I just added optional testDispatch and testState props to the context.jsx module.
new context.jsx
import React, { createContext, useReducer } from "react";
import { reducer } from "./reducer";
export const Context = createContext();
export const initialState = {
displayThings: false,
things: []
};
export const Provider = ({ children, testDispatch, testState }) => {
const [iState, iDispatch] = useReducer(reducer, initialState);
const dispatch = testDispatch ? testDispatch : iDispatch;
const state = testState ? testState : iState;
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
);
};
export default Provider;
Then in layout.test.jsx I just simply pass in mocked jest dispatch function plus state as necessary. Also removed the outer App wrapping as that seemed to prevent the props from being passed through.
new layout.test.jsx
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import Layout from './layout';
import { Provider } from './context';
describe('Layout component', () => {
it('starts with a count of 0', () => {
const dispatch = jest.fn();
const state = {
displayThings: false,
things: []
};
const { getByTestId } = render(
<Provider testDispatch={dispatch} testState={state}>
<Layout />
</Provider>
);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: "FETCH_THINGS" });
const refreshButton = getByTestId('fetch-things-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(2);
// Important: The order of the calls should be this, but dispatch is reporting them
// the opposite way around in the this test, i.e. FETCH_THINGS, then DISPLAY_THINGS...
//expect(dispatch).toHaveBeenNthCalledWith(1, { type: "DISPLAY_THINGS" });
//expect(dispatch).toHaveBeenNthCalledWith(2, { type: "FETCH_THINGS" });
// ... so as dispatch behaves correctly outside of testing for the moment I'm just settling for
// knowing that dispatch was at least called twice with the correct parameters.
expect(dispatch).toHaveBeenCalledWith({ type: "DISPLAY_THINGS" });
expect(dispatch).toHaveBeenCalledWith({ type: "FETCH_THINGS" });
});
});
One little caveat though, as noted above, when the 'fetch-things-button' was fired, it reported the dispatch in the wrong order. :/ So I just settled for knowing the correct calls where triggered, but if anyone knows why the call order isn't as expected I would be pleased to know.
https://github.com/atlantisstorm/hooks-testing update to reflect the above if anyone is interested.
A few months back I was also trying to write unit tests for reducer + context for an app. So, here's my solution to test useReducer and useContext.
FeaturesProvider.js
import React, { createContext, useContext, useReducer } from 'react';
import { featuresInitialState, featuresReducer } from '../reducers/featuresReducer';
export const FeatureContext = createContext();
const FeaturesProvider = ({ children }) => {
const [state, dispatch] = useReducer(featuresReducer, featuresInitialState);
return <FeatureContext.Provider value={{ state, dispatch }}>{children}</FeatureContext.Provider>;
};
export const useFeature = () => useContext(FeatureContext);
export default FeaturesProvider;
FeaturesProvider.test.js
import React from 'react';
import { render } from '#testing-library/react';
import { renderHook } from '#testing-library/react-hooks';
import FeaturesProvider, { useFeature, FeatureContext } from './FeaturesProvider';
const state = { features: [] };
const dispatch = jest.fn();
const wrapper = ({ children }) => (
<FeatureContext.Provider value={{ state, dispatch }}>
{children}
</FeatureContext.Provider>
);
const mockUseContext = jest.fn().mockImplementation(() => ({ state, dispatch }));
React.useContext = mockUseContext;
describe('useFeature test', () => {
test('should return present feature toggles with its state and dispatch function', () => {
render(<FeaturesProvider />);
const { result } = renderHook(() => useFeature(), { wrapper });
expect(result.current.state.features.length).toBe(0);
expect(result.current).toEqual({ state, dispatch });
});
});
featuresReducer.js
import ApplicationConfig from '../config/app-config';
import actions from './actions';
export const featuresInitialState = {
features: [],
environments: ApplicationConfig.ENVIRONMENTS,
toastMessage: null
};
const { INITIALIZE_DATA, TOGGLE_FEATURE, ENABLE_OR_DISABLE_TOAST } = actions;
export const featuresReducer = (state, { type, payload }) => {
switch (type) {
case INITIALIZE_DATA:
return {
...state,
[payload.name]: payload.data
};
case TOGGLE_FEATURE:
return {
...state,
features: state.features.map((feature) => (feature.featureToggleName === payload.featureToggleName
? {
...feature,
environmentState:
{ ...feature.environmentState, [payload.environment]: !feature.environmentState[payload.environment] }
}
: feature))
};
case ENABLE_OR_DISABLE_TOAST:
return { ...state, toastMessage: payload.message };
default:
return { ...state };
}
};
featuresReducer.test.js
import { featuresReducer } from './featuresReducer';
import actions from './actions';
const { INITIALIZE_DATA, TOGGLE_FEATURE, ENABLE_OR_DISABLE_TOAST } = actions;
describe('Reducer function test', () => {
test('should initialize data when INITIALIZE_DATA action is dispatched', () => {
const featuresState = {
features: []
};
const action = {
type: INITIALIZE_DATA,
payload: {
name: 'features',
data: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
}
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
});
});
test('should toggle the feature for the given feature and environemt when TOGGLE_FEATURE action is disptched', () => {
const featuresState = {
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}, {
featureId: '23458', featureName: 'WO photo download', featureToggleName: '23458_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
};
const action = {
type: TOGGLE_FEATURE,
payload: { featureToggleName: '23456_WOPhotoDownload', environment: 'sit' }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}, {
featureId: '23458', featureName: 'WO photo download', featureToggleName: '23458_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
});
});
test('should enable the toast message when ENABLE_OR_DISABLE_TOAST action is dispatched with the message as part of payload', () => {
const featuresState = {
toastMessage: null
};
const action = {
type: ENABLE_OR_DISABLE_TOAST,
payload: { message: 'Something went wrong!' }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({ toastMessage: 'Something went wrong!' });
});
test('should disable the toast message when ENABLE_OR_DISABLE_TOAST action is dispatched with message as null as part of payload', () => {
const featuresState = {
toastMessage: null
};
const action = {
type: ENABLE_OR_DISABLE_TOAST,
payload: { message: null }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({ toastMessage: null });
});
test('should return the current state when the action with no specific type is dispatched', () => {
const featuresState = {
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}]
};
const action = {};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}]
});
});
});
I'm trying to fetch data from firebase, then update the state of the app with the results and display the data as a list in a list component.
Everything works except the final list component displays it and immediately becomes blank again. After debugging, I found out it doesn't manage to correctly map the state to the props but I couldn't figure out how to achieve this. Thanks in advance
PlantList.js
import React, { Component } from 'react';
import PlantSummary from './PlantSummary';
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import { fetchMyPlants } from '../../store/actions/myPlantsActions'
var i =0;
class PlantList extends Component {
constructor(props) {
super(props);
this.state = { myPlants: []} ;
}
componentDidMount() {
console.log("componentDidMount() triggered & state",i,this.state);
console.log("componentDidMount() triggered & props ",i,this.props);
this.props.dispatch(fetchMyPlants());
}
render(){
i = i +1;
console.log("render()"+i,this.props,this.state);
const { myPlants } = this.props;
return(
<div className="plant-list section">
{myPlants && myPlants.map((plant) => {
return (
<Link to={'/plant/'+ plant.id}>
<PlantSummary plant={plant} key={plant.id} />
</Link>
)
})}
</div>
)
}
}
const mapStateToProps = (state) => {
console.log("mapStateToProps triggered",state);
return {
myPlants: state.myPlants.items
}
}
export default connect(mapStateToProps)(PlantList)
myPlantActions.js
export const FETCH_MY_PLANTS_BEGIN = 'FETCH_MY_PLANTS_BEGIN';
export const FETCH_MY_PLANTS_SUCCESS = 'FETCH_MY_PLANTS_SUCCESS';
export const FETCH_MY_PLANTS_FAILURE = 'FETCH_MY_PLANTS_FAILURE';
export const fetchMyPlantsBegin = () => ({
type: FETCH_MY_PLANTS_BEGIN
});
export const fetchMyPlantsSuccess = myPlants => ({
type: FETCH_MY_PLANTS_SUCCESS,
payload: { myPlants }
})
export const fetchMyPlantsFailure = err => ({
type: FETCH_MY_PLANTS_FAILURE,
payload: { err }
});
export const fetchMyPlants = () => {
return(dispatch, getState, { getFirestore }) => {
dispatch(fetchMyPlantsBegin());
const firestore = getFirestore();
const authID = getState().firebase.auth.uid;
const usersPlants = [];
firestore.collection('users').doc(authID).collection('myPlants').get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
firestore.collection('plants').doc(doc.data().id).get().then(
function(document) {
if (document.exists) {
const docToPushId = {id: doc.data().id};
let docToPush = {
...docToPushId,
...document.data()
};
usersPlants.push(docToPush);
} else {
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
}
);
});
}).then(myPlants => {
console.log("Dispatch happens now:",usersPlants);
dispatch(fetchMyPlantsSuccess(usersPlants));
return myPlants;
}).catch(error => dispatch(fetchMyPlantsFailure(error)));
}
};
myPlantsReducer.js
import {
FETCH_MY_PLANTS_BEGIN,
FETCH_MY_PLANTS_SUCCESS,
FETCH_MY_PLANTS_FAILURE
} from '../actions/myPlantsActions';
const initialState = {
items: [],
loading: false,
error: null
};
export default function myPlantsReducer(state = initialState, action) {
switch(action.type) {
case 'FETCH_MY_PLANTS_BEGIN':
return {
...state,
loading: true,
error: null
};
case 'FETCH_MY_PLANTS_SUCCESS':
return {
...state,
loading: false,
items: action.payload.myPlants
};
case 'FETCH_MY_PLANTS_FAILURE':
return {
...state,
loading: false,
error: action.payload.error,
items: []
};
default:
return state;
}
}
console logs
I have a problem. As I understood hook useEffect doen't run.
I have action that should take data from server.
export const getProducts = () => {
return dispatch => {
dispatch(getProductsStarted());
fetch('https://shopserver.firebaseapp.com/get-products')
.then(res => {
dispatch(getProductsSuccess(res.json()));
})
.catch(err => {
dispatch(getProductsFailure(err.message));
});
}
}
const getProductsSuccess = todo => ({
type: "ADD_TODO_SUCCESS",
payload: {
...todo
}
});
const getProductsStarted = () => ({
type: "ADD_TODO_STARTED"
});
const getProductsFailure = error => ({
type: "ADD_TODO_FAILURE",
payload: {
error
}
});
I have a reducer.
const initialState = {
loading: false,
products: [],
error: null
}
export const ProductReducer = (state = initialState, action) => {
switch (action.type) {
case "ADD_TODO_SUCCESS":
return {
...state,
loading: false,
error: null,
todos: [...state.products, action.payload.products]
}
case "ADD_TODO_STARTED":
return {
...state,
loading: true
}
case "ADD_TODO_FAILURE":
return {
...state,
loading: false,
error: action.payload.error
}
default:
return state
}
}
And I have a Component where I want to render a result.
import React from 'react';
import { CardItem } from "./cardItem";
import { useSelector } from 'react-redux';
import { useEffect } from 'react';
import { getProducts } from '../Redux/Actions/productAction'
export const ProductCard = () => {
useEffect(() => {
getProducts();
console.log('111111')
})
const data = useSelector(state => state.ProductReducer.products);
return (
<div>
{data.map( element =>
CardItem (element)
)}
</div>
)
}
After rendering page nothing happens. ReduxDevTools shows that there was no send actions. Please, help me to fix it. Thank you.
I think you should be calling your async action like this :
import { useDispatch, useSelector } from 'react-redux';
[...]
export const ProductCard = () => {
const dispatch = useDispatch();
useEffect(() => {
// I guess getProducts is an async redux action using redux-thunk
dispatch(getProducts());
console.log('111111')
}, []);
[...]
}
I assume you want to load products only when component is born, so I pass an empty array as second argument for useEffect (https://reactjs.org/docs/hooks-reference.html#useeffect).
I want to use 2 React Context for my app.
The 1st context is UserContext, that loads user info.
The 2nd context is ItemContext, that loads item info based on user id.
For simplicity, I use synchronous functions to load user and item info. See CodeSandbox for a full working sample.
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { ItemProvider, ItemConsumer } from "./ItemContext";
import { UserProvider, UserConsumer } from "./UserContext";
class App extends React.Component {
render() {
return (
<UserProvider>
<UserConsumer>
{({ user }) => (
<ItemProvider user={user}>
<ItemConsumer>
{({ item }) => <div>{JSON.stringify(item, null, 2)}</div>}
</ItemConsumer>
</ItemProvider>
)}
</UserConsumer>
</UserProvider>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
// UserContext.js
import React from "react";
const { Provider, Consumer } = React.createContext();
class UserProvider extends React.Component {
state = {
isLoading: false,
value: { user: {} }
};
async componentDidMount() {
this.setState({ isLoading: true });
const user = { id: 1, name: "evan" };
this.setState({ value: { user }, isLoading: false });
}
render() {
const { isLoading, value } = this.state;
if (isLoading) {
return <div>Loading data...</div>;
}
return <Provider value={value}>{this.props.children}</Provider>;
}
}
export { UserProvider, Consumer as UserConsumer };
// ItemContext.js
import React from "react";
const { Provider, Consumer } = React.createContext();
const items = { 1: { name: "a red hat" }, 2: { name: "a blue shirt" } };
const getItemByUserId = userId => items[userId];
class ItemProvider extends React.Component {
state = {
isLoading: false,
value: { item: {} }
};
async componentDidMount() {
this.setState({ isLoading: true });
const item = getItemByUserId(1);
// console.log(this.props.user); // Object {}
// const item = getItemByUserId(this.props.user.id);
this.setState({ value: { item }, isLoading: false });
}
render() {
const { isLoading, value } = this.state;
if (isLoading) {
return <div>Loading data...</div>;
}
return <Provider value={value}>{this.props.children}</Provider>;
}
}
export { ItemProvider, Consumer as ItemConsumer };
I am not able to do const item = getItemByUserId(this.props.user.id); in ItemContext because this.props.user is an empty object.
What is the solution?
User value is rendered from within UserContext asynchronously and won't be available until the children have rendered and hence you need to implement componentDidUpdate in ItemProvider to get the context value based on user prop
import React from "react";
const { Provider, Consumer } = React.createContext();
const items = { 1: { name: "a red hat" }, 2: { name: "a blue shirt" } };
const getItemByUserId = userId => items[userId];
class ItemProvider extends React.Component {
state = {
isLoading: false,
value: { item: {} }
};
async componentDidMount() {
if (this.props.user && this.props.user.id) {
this.setState({ isLoading: true });
const item = getItemByUserId(this.props.user.id);
this.setState({ value: { item }, isLoading: true });
}
}
componentDidUpdate(prevProps) {
console.log(this.props.user, prevProps.user);
if (prevProps.user !== this.props.user) {
const item = getItemByUserId(this.props.user.id);
this.setState({
value: { item }
});
}
}
render() {
const { isLoading, value } = this.state;
if (isLoading) {
return <div>Loading data...</div>;
}
console.log(value);
return <Provider value={value}>{this.props.children}</Provider>;
}
}
export { ItemProvider, Consumer as ItemConsumer };
Working demo
I'm attempting to reconfigure a PixaBay clone application to Redux. The application retrieves photos as the user types a search text. However, it breaks as soon as I type inside the input.
From what I've researched, you can only call setState in a class so I gave fetchPhotos an arrow function, but that didn't work. I also tried to .bind(this), but that gave me a parsing error. Could someone kindly tell me what I'm doing wrong? Here are the following errors, along with my code.
ERRORS
TypeError: this.setState is not a function
fetchPhotos
src/actions/actions.js:10
7 |
8 | export function fetchPhotos(e) {
9 | const url = `${ROOT_URL}/?key=${API_KEY}&q=${searchText}&image_type=photo`;
> 10 | const request = this.setState({searchText: e.target.value}, () => {
11 | axios.get(url)
12 | .then(response => {
13 | this.setState({images: response.data.hits});
fetchPhotos
node_modules/redux/es/redux.js:475
Search._this.FetchPhotosHandler [as onChange]
src/components/search/Search.js:11
8 | class Search extends Component {
9 |
10 | FetchPhotosHandler = (e) => {
> 11 | this.props.fetchPhotos(e);
12 | }
13 |
14 | render() {
SEARCH CONTAINER
import React, { Component } from 'react';
import { fetchPhotos } from '../../actions/actions';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import TextField from 'material-ui/TextField';
import ImageResults from '../imageResults/ImageResults';
class Search extends Component {
state = {
searchText: '',
images: []
}
FetchPhotosHandler = (e) => {
this.props.fetchPhotos(e);
}
render() {
return (
<div>
<TextField
name="searchText"
value={this.props.searchText}
onChange={this.FetchPhotosHandler}
floatingLabelText="Search for photos"
fullWidth={true} />
<br />
<ImageResults images={this.props.images} />
</div>
);
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ fetchPhotos, dispatch});
}
export default connect(null, mapDispatchToProps)(Search);
ACTION
import axios from 'axios';
export const FETCH_PHOTOS = 'FETCH_PHOTOS';
const ROOT_URL = 'https://pixabay.com/api';
const API_KEY = '10264275-868d83de96a4d0c47db26f9e0';
const searchText = '';
export function fetchPhotos(e) {
const url = `${ROOT_URL}/?key=${API_KEY}&q=${searchText}&image_type=photo`;
const request = this.setState({searchText: e.target.value}, () => {
axios.get(url)
.then(response => {
this.setState({images: response.data.hits});
})
.catch(error => {
console.log(error)
});
});
return {
type: FETCH_PHOTOS,
payload: request
};
}
REDUCER
import { FETCH_PHOTOS } from '../actions/actions';
const initialState = {
searchText: '',
images: []
}
const reducer = (state = initialState, action) => {
switch(action.type) {
case FETCH_PHOTOS:
return {
...state,
images: action.data.hits
};
default:
return state;
}
}
export default reducer;
You should avoid attempting to use setState() in your action as it goes against Redux entirely. setState() is meant for managing the local of a React.Component. As you are attempting to utilize Redux, you should instead dispatch actions from your actions creators that update the store via your reducers and finally mapping store values to your component's props via connect(). Below is an example of your code restructured similar to the Async Redux example.
Instead of attempting to call setState() in the action, instead an action is dispatched containing the image payload. The Search component utilizes mapStateToProps (1st argument of connect()) to map store properties such the images array to the component's props. These props are used to render a list of data. This completely eliminates the need to have an images local state property on Search as values are being retrieved from the store as changes happen via actions/reducers. This example uses redux-thunk middleware to handle async actions, but there are plenty of other options out there that you could consider.
store:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const middleware = [ thunk ];
const store = createStore(
rootReducer,
applyMiddleware(...middleware)
);
export default store;
actions:
export const FETCH_PHOTOS = 'FETCH_PHOTOS';
export const RECEIVE_PHOTOS = 'RECEIVE_PHOTOS';
// fake data
const testPhotos = [
{ id: 1, src: 'https://placehold.it/250' },
{ id: 2, src: 'https://placehold.it/250' }
];
// fake API call as promise
const getTestPhotos = () => {
return new Promise((resolve) => {
setTimeout(() => {
return resolve(testPhotos);
}, 500);
});
}
const fetchPhotos = (searchText) => ({
type: FETCH_PHOTOS
});
const receivePhotos = (photos) => ({
type: RECEIVE_PHOTOS,
data: {
hits: photos
}
});
export const searchPhotos = (searchText) => dispatch => {
// dispatch an action to handle loading/waiting for API response
dispatch(fetchPhotos(searchText));
// dispatch another action with payload within then()
return getTestPhotos()
.then(photos => dispatch(receivePhotos(photos)));
}
reducer:
import { FETCH_PHOTOS, RECEIVE_PHOTOS } from '../actions';
const initialState = {
loading: false,
searchText: '',
images: []
}
const photos = (state = initialState, action) => {
switch(action.type) {
case FETCH_PHOTOS:
return {
...state,
loading: true
};
case RECEIVE_PHOTOS:
return {
...state,
loading: false,
images: action.data.hits
};
default:
return state;
}
}
export default photos;
Search:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { searchPhotos } from './actions';
class Search extends Component {
constructor(props) {
super(props);
this.state = {
searchText: ''
};
this.fetchPhotosHandler = this.fetchPhotosHandler.bind(this);
}
fetchPhotosHandler(e) {
const { value: searchText } = e.target;
this.setState({ ...this.state, searchText }, () => {
this.props.dispatch(searchPhotos(e));
})
}
render() {
const { loading, images } = this.props;
return (
<div>
<h1>Search</h1>
<div>
<label htmlFor="search">Search:</label>
<input name="search" id="search" type="text" value={this.state.searchText} onChange={this.fetchPhotosHandler} />
</div>
{loading ? (
<div>Loading...</div>
) : (
<ul>
{images.map(image => <li key={image.id}>{image.src}</li>)}
</ul>
)}
</div>
);
}
}
const mapStateToProps = ({ photos: { loading, images } }) => ({ loading, images });
export default connect(mapStateToProps)(Search);
I've created an example to show this functionality in action at a basic level.
Hopefully that helps!
You can bind component class instance to your action and it should work.
FetchPhotosHandler = (e) => {
this.props.fetchPhotos.bind(this)(e);
}
Since you have fetchPhotos exported from different module and in order to do setState there you need to pass this context to fetchPhotos as a param and use the param to do setState. That's how this context will be available
Pass this to fetchPhotos as a param
FetchPhotosHandler = (e) => {
this.props.fetchPhotos(e, this);
}
And here access this and do seState
export function fetchPhotos(e, this) {
const url = `${ROOT_URL}/?key=${API_KEY}&q=${searchText}&image_type=photo`;
const request = this.setState({searchText: e.target.value}, () => {
axios.get(url)
.then(response => {
this.setState({images: response.data.hits});
})
.catch(error => {
console.log(error)
});
});
return {
type: FETCH_PHOTOS,
payload: request
};
}