Invalid hook call when using useEffect - reactjs

I get this error when trying to dispatch an action from my react functional component:
Invalid hook call. Hooks can only be called inside of the body of a function component
This React component does not render anything just listen to keypress event on the page
import { useDispatch } from 'react-redux';
const dispatch = useDispatch();
const {
actions: { moveDown }
} = gameSlice;
type Props = {};
export const Keyboard = ({}: Props) => {
useEffect(() => document.addEventListener('keydown', ({ keyCode }) => dispatch(moveDown())));
return <></>;
};

You will need to use the TypeScript typings for functional components, and provide the props as part of the generic typings. And don't forget to import the required modules.
import React, { useEffect } from 'react';
type Props = {};
export const Keyboard: React.FC<Props> = () => {
useEffect(() => document.addEventListener('keydown', ({ keyCode }) => dispatch(moveDown())));
return <></>;
};

Related

Create common dispatch function with react-redux

I am pretty new to redux and here I am trying to create a common dispatch function where I can call the function from multiple components but can't seem to use useDispatch() in my common component getting invalid hook call error.
import { useDispatch, useSelector } from "react-redux";
import { UPDATE_PREVIEW_DATA } from "../../redux/types";
export default function setPreviewData(event, obj, lang) {
const dispatch = useDispatch();
const previewData = useSelector((state) => state.previewData);
const dispatchFunc = () => {
dispatch({
type: UPDATE_PREVIEW_DATA,
data: {
[obj]: {
[lang]: {
...previewData[obj][lang],
[event.target.name]: event.target.value,
},
},
},
});
};
return dispatchFunc;
}
// previewData.js in action folder
import { UPDATE_PREVIEW_DATA } from "../types";
const previewData = (data) => (dispatch) => {
dispatch({
type: UPDATE_PREVIEW_DATA,
data,
});
};
export default previewData;
// previewData.js in reducers folder
import { UPDATE_PREVIEW_DATA } from "../types";
const initialState = {...};
const previewData = (state = initialState, action) => {
switch (action.type) {
case UPDATE_PREVIEW_DATA: {
return action.data;
}
default:
return state;
}
};
export default previewData;
And I am trying to make this work like
// component.jsx
setPreviewData(e, "hightlights", "en");
Hooks are intended to be used in Functional components only. As per the Rules of hooks they can be called from
React function components.
Custom Hooks
Reference -> https://reactjs.org/docs/hooks-rules.html#only-call-hooks-from-react-functions
now you might think your setPreviewData is a React Function Component, but it's just a normal js function, that's why you are getting the error.
As a result, it doesn't get wrapped in React.createElement, so it thinks the hook call is invalid.
Moreover, you are committing one more mistake here, lets's say if setPreviewData was a Function Component you still call it as if though its a normal function

Cant use a useDispatch inside a useEffect

So Im receiving the following error when using a useDispatch inside a useEffect
Line 58:5: React Hook "useDispatch" cannot be called inside a
callback. React Hooks must be called in a React function component or
a custom React Hook function react-hooks/rules-of-hooks
I have a component like follows
import { useSelector, useDispatch } from 'react-redux';
import { myModules } from '#/redux/modules';
const Component = () => {
const images = useSelector(getImages)
useEffect( () => {
useDispatch(myModules.actions.setMaskVisible(true));
}, [images]);
}
setMaskVisible is pretty much
export const SET_MASK_VISIBLE = 'caps/SET_MASK_VISIBLE';
const setMaskVisible = payload => {
return {
type: SET_MASK_VISIBLE,
payload
};
};
export default { setMaskVisible }
Im basically trying to change a redux value, when images update. Why desnt it work?
Have a look at the documentation. You call useDispatch to get a function and call that function:
import { useSelector, useDispatch } from 'react-redux';
import { myModules } from '#/redux/modules';
const Component = () => {
const images = useSelector(getImages)
const dispatch = useDispatch();
useEffect( () => {
dispatch(myModules.actions.setMaskVisible(true));
}, [images]);
}

react cannot put parameter in function (redux)

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.

Mocking React custom hook with Jest

I need to mock my custom hook when unit testing React component. I have read few tutorials and stackoverflow answers to this simple task, but without luck to implement it correctly.
My simplest possible setup for single test is as following:
// TestComponent.js
import React from "react";
import useTest from "./useTest";
const TestComponent = () => {
const { state } = useTest("initial_value");
return <div>{state}</div>;
};
export default TestComponent;
// useTest.jsx - simple custom hook
import React, { useState } from "react";
const useTest = (initialState) => {
const [state] = useState(initialState);
return { state };
};
export default useTest;
// TestComponent.test.jsx - my test case
import React from "react";
import { render } from "#testing-library/react";
import TestComponent from "./TestComponent";
jest.mock("./useTest", () => ({
useTest: () => "mocked_value",
}));
test("rendertest", () => {
const component = render(<TestComponent />);
expect(component.container).toHaveTextContent("mocked_value");
});
So I trying to mock useTest custom hook to return "mocked_value", instead of "initial_value" from real custom hook. But above code just gives me this error:
TypeError: (0 , _useTest.default) is not a function
3 |
4 | const TestComponent = () => {
> 5 | const { state } = useTest("initial_value");
| ^
6 |
7 | return <div>{state}</div>;
8 | };
I have also tried:
import useTest from './useTest';
// ...
jest.spyOn(useTest, "useTest").mockImplementation(() => "mocked_value");
import useTest from './useTest';
// ...
jest.spyOn(useTest, "useTest").mockReturnValue("mocked_value");
But both gives me error Cannot spy the useTest property because it is not a function; undefined given instead.
How do I implement this test?
I'm answering to myself. This way it's working:
jest.mock("./useTest", () => ({
useTest: () => ({ state: 'mocked_value' }),
}));
And if I want to use default export in custom hook:
jest.mock("./useTest", () => ({
__esModule: true,
default: () => ({ state: 'mocked_value' }),
}));
Also, if I want to also use setState method in my hook and export it, I can mock it like this:
const mockedSetState = jest.fn();
jest.mock("./useTest", () => ({
useTest: () => ({ state, setState: mockedSetState }),
}));
And now it's possible to check if setState has been called once:
expect(mockedSetState).toHaveBeenCalledTimes(1);
Since you use export default useTest in useTest module, it expects to get that function reference from a default attribute in your mock.
Try this:
jest.mock("./useTest", () => ({
default: () => "mocked_value",
}));
If you want to avoid confusion, you could try export const useTest = ... in useTest module and then import { useTest } from './useTest' in your component. No need to change your test if using this approach.
For those who use the right hook it can be done this way
import * as useFetchMock from "../../../../hooks/useFetch";
it("should show loading component on button when state is loading", () => {
jest.spyOn(useFetchMock, "default").mockReturnValue({
result: null,
message: null,
status: "pending",
loading: true,
fetch: jest.fn(),
});
render(<Component />);
expect.assertions(1);
expect(screen.getByTestId(testIdComponentB)).toBeInTheDocument();
});
import your custom hook directly and use 'default' to interact directly

React: Getting Initial State in Class based compopnents

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.

Resources