"React has detected a change in the order of Hook" - reactjs

I encountered this issue while writing the test for the Auth Component. Not sure if its an issue with the way I write the test or an issue in the application itself. Below is the error.
Previous render Next render
------------------------------------------------------
1. useState useState
2. undefined useEffect
and I see this message on the failed tests. "Rendered more hooks than during the previous render."
When I added console statements on the useEffect, I see it's invoked only once.
Below is my Auth component.
const Auth: React.FC<Props> = (props) => {
const state = useConsoleState();
const [problemType, setProblemType] = React.useState<ProblemTypeEnum>("TECH");
React.useEffect(() => {
switch (props.supportType) {
case "limit":
setProblemType("LIMIT");
break;
case "account":
setProblemType("ACCOUNT");
break;
default:
setProblemType("TECH");
break;
}
}, [props.supportType]);
};
const userId = getUserOcid(state.userProfile);
const cimsUserValidationResult = authApiCalls.callCims(!props.csi, props.csi, userId, problemType, state.homeRegionName);
and my test
describe("Auth tests", () => {
beforeEach(() => {
const useEffect = jest.spyOn(React, "useEffect");
useEffect.mockImplementationOnce(f => f());
authApiCalls.callCims = jest.fn().mockReturnValue({
loading: false,
response: {
response: {
ok: true,
},
},
} as QueryResult<ValidationResponse>);
});
const runProblemTypeTest = (url: string, supportType: SupportType) => {
window = Object.create(window);
Object.defineProperty(window, "location", {
value: {
href: url,
},
writable: true
});
mount(
<Auth supportType=. {supportType}>
<div>TestChild</div>
</Authorization>
);
expect(authApiCalls.callCims).toHaveBeenCalledWith(
false,
"1234",
"test",
supportType,
"homeRegion"
);
};
it("check problem type passed to validate when problem type absent in the url", () => {
runProblemTypeTest("http://www.test.com", "tech");
});
});

Related

Testing Optimistic update in react query

I am trying to write the test case for an optimistic update in react query. But it's not working. Here is the code that I wrote to test it. Hope someone could help me. Thanks in advance. When I just write the onSuccess and leave an optimistic update, it works fine but here it's not working. And how can we mock the getQueryData and setQueryData here?
import { act, renderHook } from "#testing-library/react-hooks";
import axios from "axios";
import { createWrapper } from "../../test-utils";
import { useAddColorHook, useFetchColorHook } from "./usePaginationReactQuery";
jest.mock("axios");
describe('Testing custom hooks of react query', () => {
it('Should add a new color', async () => {
axios.post.mockReturnValue({data: [{label: 'Grey', id: 23}]})
const { result, waitFor } = renderHook(() => useAddColorHook(1), { wrapper: createWrapper() });
await act(() => {
result.current.mutate({ label: 'Grey' })
})
await waitFor(() => result.current.isSuccess);
})
})
export const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: Infinity,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {},
}
});
export function createWrapper() {
const testQueryClient = createTestQueryClient();
return ({ children }) => (
<QueryClientProvider client={testQueryClient}>
{children}
</QueryClientProvider>
);
}
export const useAddColorHook = (page) => {
const queryClient = useQueryClient()
return useMutation(addColor, {
// onSuccess: () => {
// queryClient.invalidateQueries(['colors', page])
// }
onMutate: async color => {
// newHero refers to the argument being passed to the mutate function
await queryClient.cancelQueries(['colors', page])
const previousHeroData = queryClient.getQueryData(['colors', page])
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
}
})
return { previousHeroData }
},
onSuccess: (response, variables, context) => {
queryClient.setQueryData(['colors', page], (oldQueryData) => {
console.log(oldQueryData, 'oldQueryData', response, 'response', variables, 'var', context, 'context', 7984)
return {
...oldQueryData,
data: oldQueryData.data.map(data => data.label === variables.label ? response.data : data)
}
})
},
onError: (_err, _newTodo, context) => {
queryClient.setQueryData(['colors', page], context.previousHeroData)
},
onSettled: () => {
queryClient.invalidateQueries(['colors', page])
}
})
}
The error that you are getting actually shows a bug in the way you've implemented the optimistic update:
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
}
})
what if there is no entry in the query cache that matches this query key? oldQueryData will be undefined, but you're not guarding against that, you are spreading ...oldQueryData.data and this will error out at runtime.
This is what happens in your test because you start with a fresh query cache for every test.
An easy way out would be, since you have previousHeroData already:
const previousHeroData = queryClient.getQueryData(['colors', page])
if (previousHeroData) {
queryClient.setQueryData(['colors', page], {
...previousHeroData,
data: [...previousHeroData.data, { id: previousHeroData.data.length + 1, ...color }]
}
}
If you are using TanStack/query v4, you can also return undefined from the updater function. This doesn't work in v3 though:
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return oldQueryData ? {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
} : undefined
})
This doesn't perform an optimistic update then though. If you know how to create a valid cache entry from undefined previous data, you can of course also do that.

React jest trying to test if the function in useDisapatch has been called

Hi All I am trying to test a function that is dispatched by useDispatch. I have a helper function that can dispatch one or another function based on the values passed in.
I can mock the actually useDispatch and test if it has been called and its payload. But i though to also test the function that is actually dispatched.
When i try to do that i get this error:
Expected: {"payload": "", "type": "calculatorNumber/addSecondNumber"}
Received: undefined
Helper function that i am trying to test:
import { addFirstNumber, addSecondNumber } from "../features/calculatorSlice";
const addFirstOrSecondNumber = (value, symbolValue, dispatch) => {
symbolValue.length !== 0
? dispatch(addSecondNumber(value))
: dispatch(addFirstNumber(value));
};
export default addFirstOrSecondNumber;
Test:
import addFirstOrSecondNumber from "./addFirstOrSecondNumber";
import { addFirstNumber, addSecondNumber } from "../features/calculatorSlice";
// mock useDispatch
const mockDispatch = jest.fn();
// const mockAddFirstNumber = jest.fn();
// const mockAddSecondNumber = jest.fn();
jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"),
useDispatch: () => mockDispatch,
}));
jest.mock("../features/calculatorSlice");
describe("addFirstOrSecondNumber", () => {
test("if the user does not select symbol character the first number will be added", () => {
const payloadMock = {
payload: "4",
type: "calculatorNumber/addFirstNumber",
};
addFirstOrSecondNumber("4", "", mockDispatch);
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(addFirstNumber).toHaveBeenCalledTimes(1);
expect(mockDispatch).toHaveBeenCalledWith(payloadMock);
});
test("if the user selects symbol second number will be added", () => {
const payloadMock = {
payload: "",
type: "calculatorNumber/addSecondNumber",
};
addFirstOrSecondNumber("", "/", mockDispatch);
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(addSecondNumber).toHaveBeenCalledTimes(1);
expect(mockDispatch).toHaveBeenCalledWith(payloadMock);
});
});
Not sure if that matters but i am using redax toolkit

Can I use HOC programming style on a functional component?

I have a class component that's empowered with the HOC withRouter, and connect() to have dispatch available in props
export default withRouter(connect()(MyComponent));
This component is coded as a class component, now, I changed the component to a functional component:
Changed the function header from class "MyComponent expands..." to "const MyComponent = props => {..."
Changed the way the state is created, by using "const [state, setState] = useState(..."
Instead of coding componentDidMount to perform initial operations, I use
useEffect(() => {
getData()
}, []);
Where getData is:
const getData = props => {
const getDocument = async () => {
const {
dispatch,
location: { search }, // Here I get an error, search and
match: { params }, // params are undefined
} = props;
...
}
Changed every function definition, from "handleSuccessAction = message => {..." to "const handleSuccessIndex = response => {..."
Changed all reference to functions from "this.myFunction();" to "myFunction()"
And leaved export default withRouter(connect()(MyComponent)) as is
Begin EDIT:
here is the whole code, once turned into a functional component
------------- all imports ------------------
const DocumentPreview = props => {
const [state, setState] = useState({
document: {
documentType: '',
name: '',
file: {
fileUrl: '',
filename: ''
},
},
file: {
fileUrl: '',
filename: ''
},
});
useEffect(() => {
getDocument();
}, []);
const getDocument = async () => {
const {
dispatch,
location: { search },
match: { params },
} = props;
const options = parseUrlParams(search);
setState({ onRequest: true });
showDocumentPreviewRequest(params.id, {
employeeId: options.employee,
dispatch,
successCallback: handleSuccessIndex
});
};
const handleSuccessIndex = response => {
const { data } = response;
const document = camelCaseRecursive(data);
const file = document.fileInfo;
setState({
document,
file,
onRequest: false
});
};
const { onRequest, document, file, modalBody, modalShow, modalTitle, defaultModalShow } = state;
const { pendingToApprove, workflowRequest } = document;
return (
<>
------- Do my stuff -------------
</>
);
}
export default withRouter(connect()(DocumentPreview))
End EDIT
Why do I get this error?, am I doing it wrong?
I'm changing it to a functional component since I have to load the component on a modal, and as I won't be using router and render props, I think I can get this props by using hooks
Rafael

Stale closure problem react native AppState

I have a react native component that I want to check if a specific value is "true" in scope state, and then do some things but it always read old state in callback function.
const [scope, seScope] = useState({
isInScope: false,
loading: true,
isGranted: false,
})
const stateHandler = useCallback((state) => {
if (state === 'active') {
// it's always false not matter
if (!scope.isGranted) {
makeLocationPermission()
}
}
}, [scope])
console.log(scope.isGranted)
// in here isGranted is true
useEffect(() => {
makeIsGrantedTrue()
AppState.addEventListener('change', stateHandler)
return () => {
AppState.removeEventListener('change', stateHandler)
}
}, [])
ok I solve this just by putting "scope" on use effect dependency.
I don't know why ?! it must be on useCallback dependency.
useEffect(() => {
makeIsGrantedTrue()
AppState.addEventListener('change', stateHandler)
return () => {
AppState.removeEventListener('change', stateHandler)
}
}, [scope])

How to mock react custom hook returned value?

Here is my custom hook:
export function useClientRect() {
const [scrollH, setScrollH] = useState(0);
const [clientH, setClientH] = useState(0);
const ref = useCallback(node => {
if (node !== null) {
setScrollH(node.scrollHeight);
setClientH(node.clientHeight);
}
}, []);
return [scrollH, clientH, ref];
}
}
I want each time that it is called, it return my values. like:
jest.mock('useClientRect', () => [300, 200, () => {}]);
How can I achieve this?
Load the hook as a module. Then mock the module:
jest.mock('module_name', () => ({
useClientRect: () => [300, 200, jest.fn()]
}));
mock should be called on top of the file outside test fn. Therefore we are going to have only one array as the mocked value.
If you want to mock the hook with different values in different tests:
import * as hooks from 'module_name';
it('a test', () => {
jest.spyOn(hooks, 'useClientRect').mockImplementation(() => ([100, 200, jest.fn()]));
//rest of the test
});
Adding on to this answer for typescript users encountering the TS2339: Property 'mockReturnValue' does not exist on type error message. There is now a jest.MockedFunction you can call to mock with Type defs (which is a port of the ts-jest/utils mocked function).
import useClientRect from './path/to/useClientRect';
jest.mock('./path/to/useClientRect');
const mockUseClientRect = useClientRect as jest.MockedFunction<typeof useClientRect>
describe("useClientRect", () => {
it("mocks the hook's return value", () => {
mockUseClientRect.mockReturnValue([300, 200, () => {}]);
// ... do stuff
});
it("mocks the hook's implementation", () => {
mockUseClientRect.mockImplementation(() => [300, 200, () => {}]);
// ... do stuff
});
});
Well, this is quite tricky and sometimes developers get confused by the library but once you get used to it, it becomes a piece of cake. I faced a similar issue a few hours back and I'm sharing my solution for you to derive your solution easily.
My custom Hook:
import { useEffect, useState } from "react";
import { getFileData } from "../../API/gistsAPIs";
export const useFilesData = (fileUrl: string) => {
const [fileData, setFileData] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
setLoading(true);
getFileData(fileUrl).then((fileContent) => {
setFileData(fileContent);
setLoading(false);
});
}, [fileUrl]);
return { fileData, loading };
};
My mock code:
Please include this mock in the test file outside of your test function.
Note: Be careful about the return object of mock, it should match with the expected response.
const mockResponse = {
fileData: "This is a mocked file",
loading: false,
};
jest.mock("../fileView", () => {
return {
useFilesData: () => {
return {
fileData: "This is a mocked file",
loading: false,
};
},
};
});
The complete test file would be:
import { render, screen, waitFor } from "#testing-library/react";
import "#testing-library/jest-dom/extend-expect";
import FileViewer from "../FileViewer";
const mockResponse = {
fileData: "This is a mocked file",
loading: false,
};
jest.mock("../fileView", () => {
return {
useFilesData: () => {
return {
fileData: "This is a mocked file",
loading: false,
};
},
};
});
describe("File Viewer", () => {
it("display the file heading", async () => {
render(<FileViewer fileUrl="" filename="regex-tutorial.md" className="" />);
const paragraphEl = await screen.findByRole("fileHeadingDiplay");
expect(paragraphEl).toHaveTextContent("regex-tutorial.md");
});
}

Resources