First, I am really new to react; so, apologies, for beginner questions.
I have a React app with Redux and Redux Saga.
One of the components looks like this:
import { TableContainer, TableHead, TableRow } from '#material-ui/core';
import Paper from '#material-ui/core/Paper';
import makeStyles from '#material-ui/core/styles/makeStyles';
import Table from '#material-ui/core/Table';
import TableBody from '#material-ui/core/TableBody';
import TableCell from '#material-ui/core/TableCell';
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { ProgramCategory } from '../model/program-category';
import { ProgramCategoryItemRow } from '../ProgramGategoryItemRow/ProgramCategoryItemRow';
import { ProgramCategoryActions } from '../store/program-category.actions';
import { ProgramCategorySelectors } from '../store/program-category.selectors';
const useStyles = makeStyles({
table: {
width: '100%',
},
tableHeadCell: {
fontWeight: 'bold',
},
});
export interface ProgramCategoriesTableProps {
isLoaded: boolean;
categories: ProgramCategory[];
fetchAllCategories: () => void;
}
export const PureProgramCategoriesTable: React.FC<ProgramCategoriesTableProps> = ({
isLoaded,
categories,
fetchAllCategories,
}) => {
useEffect(() => {
console.error('in useEffect');
fetchAllCategories();
});
const styles = useStyles();
return (
<TableContainer component={Paper}>
// the rest
<TableBody>
{categories.map(c => (
<ProgramCategoryItemRow category={c} />
))}
</TableBody>
</TableContainer>
);
};
const mapStateToProps = createSelector(
[ProgramCategorySelectors.isLoaded, ProgramCategorySelectors.getAll],
(isLoaded, categories) => ({ isLoaded, categories }),
);
const mapDispatchToProps = {
fetchAllCategories: ProgramCategoryActions.fetchAll.start,
};
export const ProgramCategoriesTable = connect(mapStateToProps, mapDispatchToProps)(PureProgramCategoriesTable);
The sagas that process ProgramCategoryActions.fetchAll.start is as follows:
import { call, put, takeLatest } from 'redux-saga/effects';
import { ProgramCategoryApi } from '../services/program-category.api';
import { ProgramCategoryActions } from './program-category.actions';
function* handleFetchAll() {
try {
const categories = yield call(ProgramCategoryApi.fetchAll);
yield put(ProgramCategoryActions.fetchAll.success(categories));
} catch (e) {
yield put(ProgramCategoryActions.fetchAll.failure(e));
}
}
export function* programCategorySagas() {
yield takeLatest(ProgramCategoryActions.fetchAll.start.type, handleFetchAll);
}
Everything make sense, but what happens my action code is executed over and over again. Digging into it a bit more, it appears that the effect hook is also executed over and over again.
If I understand it correctly, it happens because the data in state is changing, the component is getting re-rendered again. But, it leads to infinite loop.
What am I doing wrong? What is the correct way to setup this kind of component?
One of the options that I found is to change the saga to:
function* handleFetchAll() {
try {
const alreadyLoaded = select(ProgramCategorySelectors.isLoaded);
if (!alreadyLoaded) {
const categories = yield call(ProgramCategoryApi.fetchAll);
yield put(ProgramCategoryActions.fetchAll.success(categories));
}
} catch (e) {
yield put(ProgramCategoryActions.fetchAll.failure(e));
}
}
So, it only calls the api once; and it seem to work fine this way. But, is it the correct solution?
As suggested in the comments, I tried adding dependency to the effect:
useEffect(() => {
fetchAllCategories();
}, []);
Now, I am getting an error:
./src/program/ProgramCategoriesTable/ProgramCategoriesTable.tsx Line
37:6: React Hook useEffect has a missing dependency:
'fetchAllCategories'. Either include it or remove the dependency
array. If 'fetchAllCategories' changes too often, find the parent
component that defines it and wrap that definition in useCallback
react-hooks/exhaustive-deps
The issue is here,
useEffect(() => {
console.error('in useEffect');
fetchAllCategories();
});
From react docs: Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. (We will later talk about how to customize this.) Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.
https://reactjs.org/docs/hooks-effect.html
You have to pass and array of dependecies at the end.
useEffect(() => {
console.error('in useEffect');
fetchAllCategories();
}, []);
Hope this helps!
Related
When I clicked add to cart button for a individual product then show me error like this:
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component. This could happen for one of the following
reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug
and fix this problem.
I have tried like this:
here is my cartScreen.js code
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { addToCart } from "../action/cartAction";
const CartScreen = (props) => {
const productId = props.match.params.id;
const qty = props.location.search
? Number(props.location.search.split("=")[1])
: 1;
const disptach = useDispatch;
useEffect(() => {
if (productId) {
disptach(addToCart(productId, qty));
}
}, [disptach, productId, qty]);
return (
<div>
<h2>Cart Screen</h2>
<p>
Add to Cart : ProductId: {productId} Qty: {qty}
</p>
</div>
);
};
export default CartScreen;
But When I have comment out useEffect hook then does not show me any error:
useEffect(() => {
if (productId) {
disptach(addToCart(productId, qty));
}
}, [disptach, productId, qty]);
Any suggestion please.
You need to change this from:
const disptach = useDispatch;
to:
const disptach = useDispatch(); // useDispatch() is a hook
Let me know if it works, good Luck
I have this React Component
import React, { useState, useEffect } from 'react';
import axios from "axios";
import "../../css/driversStandings.css";
function DriversStandingsComponent() {
const [data, setData] = useState([]);
var row = 1;
useEffect(() => {
axios.get("http://localhost:4000/api/standings").then(res => {
const driversChampionshipData = res.data[0].DriversChampionship
setData(driversChampionshipData);
console.log(data)
})
});
return (
//Here I return a mdbootstrap table, mapping the data array
)
}
export default DriversStandingsComponent;
I don't really understand why this happens, and if it affects the server performance.
Any idea for solving this? I don't even know if it's an error itself 😅
useEffect is called every time a component rerenders. You sholud add empty dependency array, that way useEffect calls only when component is mounted, like this:
useEffect(() => {
axios.get("http://localhost:4000/api/standings").then(res => {
const driversChampionshipData = res.data[0].DriversChampionship
setData(driversChampionshipData);
console.log(data)
})
}, []);
I have the following component in React
import React from 'react'
import { Row, Col, Form, Input, Button, Card } from 'antd';
import { FormComponentProps } from 'antd/lib/form/Form';
import Particles from 'react-particles-js';
import { useTranslation } from "react-i18next";
import { connect } from 'react-redux';
import { RootState } from '../../services/store/rootReducer';
import { UsersActions } from '../../services/store';
interface LoginProps extends FormComponentProps {
rootState: RootState
}
class Login extends React.Component<LoginProps> {
state = { email: '', password: ''};
changeHandler = (e: any, name: any) => {
var value = e.target.value;
this.setState({[name]: value})
}
loginUser = () => {
try {
UsersActions.loginRequestAsync(this.state, (token: any) => {
console.log(token);
});
} catch(exception)
{
console.log(exception)
}
}
render() {
const { t } = useTranslation();
const { getFieldDecorator } = this.props.form;
return (
<div>
///blabla
</div>
)
}
}
const mapStateToProps = (state: RootState) => ({
rootState: state
});
const mapDispatchToProps = {}
const createdForm = Form.create()(Login);
export default connect(
mapStateToProps,
mapDispatchToProps
)(createdForm);
When I add the line
const { t } = useTranslation();
The app do not compile with
×
Invalid hook call. Hooks can only be called inside of the body of a
function component. This could happen for one of the following
reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
fix this problem.
Now, I tried to understand the rule, hooks must be called only on top level of a component in order for react to always load the component the same way. But where is my top level ?
I tried to put outside of render and as a property of the component, I still have the same loading error.
You broke the rules of Hooks, namely: No Hooks in classes.
That should really be the trick here. Try to rewrite it to something like the following:
function Login(props: LoginProps) {
const [data, setData] = useState({ email: '', password: '' });
const { t } = useTranslation();
const loginUser = () => { /* ... */ };
render() {
return <div>
{/*...*/ }
</div>
}
}
On the document pages, there is even a page only on Hook Errors/Warnings: Invalid Hook Call Warning
In Breaking the Rules of Hooks it states:
🔴 Do not call Hooks in class components.
🔴 Do not call in event handlers.
🔴 Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.
Hooks are used in functional components, here you have a class component that's why it's throwing an error here, error is saying it
Hooks can only be called inside of the body of a function component.
Hope it helps
I need to write a test with the following steps:
get user data on mount
get project details if it has selectedProject and clientId when they change
get pages details if it has selectedProject, clientId, and selectedPages when they change
render Content inside Switch
if doesn't have clientId, Content should return null
if doesn't have selectedProject, Content should return Projects
if doesn't have selectedPages, Content should return Pages
else Content should render Artboard
And the component looks like this:
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getUserData } from "../../firebase/user";
import { selectProject } from "../../actions/projects";
import { getItem } from "../../tools/localStorage";
import { getProjectDetails } from "../../firebase/projects";
import { selectPages } from "../../actions/pages";
import Pages from "../Pages";
import Projects from "../Projects";
import Artboard from "../Artboard";
import Switch from "../Transitions/Switch";
import { getUserId, getClientId } from "../../selectors/user";
import { getSelectedProject } from "../../selectors/projects";
import { getSelectedPages, getPagesWithDetails } from "../../selectors/pages";
import { getPagesDetails } from "../../firebase/pages";
const cachedProject = JSON.parse(getItem("selectedProject"));
const cachedPages = JSON.parse(getItem("selectedPages"));
const Dashboard = () => {
const dispatch = useDispatch();
const userId = useSelector(getUserId);
const clientId = useSelector(getClientId);
const selectedProject = useSelector(getSelectedProject) || cachedProject;
const selectedPages = useSelector(getSelectedPages) || cachedPages;
const pagesWithDetails = useSelector(getPagesWithDetails);
useEffect(() => {
dispatch(
getUserData(userId)
);
cachedProject && selectProject(cachedProject);
cachedPages && selectPages(cachedPages);
}, []);
useEffect(() => {
if (selectedProject && clientId) {
dispatch(
getProjectDetails(
clientId,
selectedProject
)
);
}
}, [selectedProject, clientId]);
useEffect(() => {
if (selectedPages && selectedProject && clientId) {
const pagesWithoutDetails = selectedPages.filter(pageId => (
!Object.keys(pagesWithDetails).includes(pageId)
));
dispatch(
getPagesDetails(
selectedProject,
pagesWithoutDetails
)
);
}
}, [selectedPages, selectedProject, clientId]);
const Content = () => {
if (!clientId) return null;
if (!selectedProject) {
return <Projects key="projects" />;
}
if (!selectedPages) {
return <Pages key="pages" />;
}
return <Artboard key="artboard" />;
};
console.log("Update Dashboard")
return (
<Switch>
{Content()}
</Switch>
);
};
Where I use some functions to fetch data from firebase, some to dispatch actions, and some conditionals.
I'm trying to get deep into testing with Jest and Enzyme. When I was searching for testing approaches, testing useEffect, variables, and conditions, I haven't found anything. All I saw is testing if a text changes, if a button has get clicked, etc. but what about testing components which aren't really changing anything in the DOM, just loading data, and depending on that data, renders a component?
What's the question here? What have you tried? To me it seems pretty straightforward to test:
Use Enzymes mount or shallow to render the component and assign that to a variable and wrap it in a store provider so it has access to a redux store.
Use jest.mock to mock things you don't want to actually want to happen (like the dispatching of actions) or use something like redux-mock-store.
Use that component ".find" to get the actual button you want.
Assert that, given a specific redux state, it renders correctly.
Assert that actions are dispatched with the proper type and payload at the proper times.
You may need to call component.update() to force it to rerender within the enzyme test.
Let me know if you have more specific issues.
Good luck!
I just started to work on React Js and Redux-Thunk. Currently, I am trying to fetch data from a url using redux-thunk. I got data successfully but the issue is that it renders undefined data twice, then it gives me the desired data in props.
Here is my code.
In Actions
index.js
function getData() {
return {
type: 'FETCH'
}
}
function getSuccess(data) {
return {
type: 'FETCH_SUCCESS',
payload: data
}
}
function getFailed(err) {
return {
type: 'FAILED',
payload: err
}
}
export function fetchData() {
const thunk = async function thunk(dispatch) {
try {
dispatch(getData());
const body = await fetch("http://jsonplaceholder.typicode.com/users")
const res = await body.json();
console.log("Thunk", res);
dispatch(getSuccess(res));
}
catch(err) {
dispatch(getFailed(err));
}
}
return thunk;
}
In Reducers fetch.js
const initialState = {
state: []
}
export default function(state=initialState , actions) {
switch(actions.type) {
case 'FETCH':
return {
...state
}
case 'FETCH_SUCCESS':
return {
...state,
data: actions.payload
}
case 'FAILED':
return {
...state
}
default:
return state;
}
}
Reducers Index.js
import fetch from './fetch';
import { combineReducers } from 'redux';
const rootReducer = combineReducers ({
fetch
});
export default rootReducer;
App.js
import React, { Component } from 'react';
import './App.css';
import Main from './component/Main';
import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
console.log("STore", store.getState());
class App extends Component {
render() {
return (
<Provider store={store}>
<Main/>
</Provider>
);
}
}
export default App;
Main.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../actions';
class Main extends Component {
componentWillMount() {
const { fetchData } = this.props
fetchData();
}
render() {
let mydata = this.props.data.data;
console.log("Data .....<>", mydata);
return(
<div>
Main
</div>
);
}
}
const mapStateToProps =(state)=> {
return {
data: state.fetch
}
}
export default connect(mapStateToProps, {fetchData: actions.fetchData})(Main);
Output Screen Shot
Please let me know what i am doing wrong. Any help will be appreciated.
Thanks
This behavior is correct. You're doing everything in the normal way, except calling async operations in componentWillMount method instead of componentDidMount.
Read more here about it.
You need to know, that when you are using componentDidMount - you are handle this in a safe way due to commit phase in component lifecycle and it means that your request will be guaranteed trigger once instead of possible several times, which can be in render phase.
See here the visual diagram to understand this more.
Regarding several renderings - it can be explained easily.
First time, when your component is mounting you are calling asynchronous operation and it needs more time to load data than for component mounting. Thats why you are accessing data property of your initialState (which is empty array), and getting undefined.
When you response is ready and actions is being dispatched, redux trigger re-render of connected components with new state.
If you want to make your async-await syntax works you should make lifecycle with async keyword as well and await your request inside.
NOTE: It's safe, but in several cases it might cause unexpected bugs, so keep in mind. Nevertheless, I don't recommend to use it in a such way. Read about this topic in the another thread at SO.
I advice you to create some isLoading boolean status in your reducer and keep track whether data is loaded or not.
Good luck! Hope it will helps!
There is nothing wrong with your code, but there is one unnecessary action.
Why do you see two times undefined and then you see your data?
First one is coming from the initial render. You are making an async dispatching in your componentWillMount so render does not wait for it, then try to log your data. At that time it is undefined.
Your fetchData dispatches two actions: getData and getSuccess. You see second undefined because of getData action since it returns current state which props.data.data undefined at that time again.
You see the data since getSuccess updates your state now.
If you want to test this comment out getData in your fetchData function, just use getSuccess and change your console.log in the render like that:
mydata && console.log("Data .....<>", mydata);
I think getData action is unnecessary here, it does not do anything beside returning the current state. Also, try to use componentDidMount instead of componentWillMount since it will be deprecated in version 17.