React Hook : Dependency injection - reactjs

For testing purposes, I setup a dependency injection on my react component. I'm not sure if it's the best practice.
I got this warning React Hook useEffect has a missing dependency:
Is there a better way to fix or to make my intent?
In my basic example, I have a service that fetch an "hello world" on an Api. My component use the service to fetch the data on loading.
I can easly test my component thanks to the dependency injection (with props), by inject some mock function.
## hello-world.js
import React, {useEffect, useState} from "react";
import {fetchHelloWorld} from "../services/fetch-hello-world";
import PropTypes from 'prop-types';
const HelloWorld = ({
fetchHelloWorld
}) => {
const [message, setMessage] = useState('');
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchHelloWorld().then(message => {
setIsLoading(false);
setMessage(message.response);
})
}, [fetchHelloWorld]
);
return (
<>
{isLoading ? "Loading" : message}
</>
);
}
HelloWorld.defaultProps = {
fetchHelloWorld: fetchHelloWorld
}
HelloWorld.propTypes = {
fetchHelloWorld: PropTypes.func.isRequired
}
export default HelloWorld;
## fetch-hello-world.js
export function fetchHelloWorld() {
return fetch("/controller/hello_world")
.then(res => res.json())
.catch(e => console.error(e));
}
in this way i can test my component like that :
import React from "react";
import {act, render, screen} from "#testing-library/react";
import HelloWorld from "./hello-world";
describe("Hello Wolrd", () => {
test('should display Loading when data not load', async () => {
render(<HelloWorld/>);
const linkElement = screen.getByText('Loading');
expect(linkElement).toBeInTheDocument();
});
test('should display data when loaded', async () => {
let fakeFetchHelloWorld = () => Promise.resolve({response: "Hello World"});
await act(async () => {
render(<HelloWorld fetchHelloWorld={fakeFetchHelloWorld}/>);
})
const linkElement = screen.getByText('Hello World');
expect(linkElement).toBeInTheDocument();
});
})

Related

Next.js useContext issue

I'm trying to provide my data via ContextProvider to my own reactComponent. I've create Context.jsx (I wanted to have external context file). But when I try to connect my Context.jsx with _app.jsx I have an arror:
Could not find a declaration file for module './Context.jsx'. 'Context.jsx' implicitly has an 'any' type.ts(7016)
And here below the code of my Context.jsx:
import React, { createContext, useState, useEffect, useContext } from "react";
const Context = createContext();
const Provider = ({ children }) => {
// the value that will be given to the context
const [code, setCode] = useState(null);
useEffect(() => {
const fetchBlogs = () => {
fetch(`https://node-test-mongo.herokuapp.com/api/blog`)
.then((response) => {
return response.json();
})
.then((data) => {
setCode(data.blogs)
})
.catch((error) => console.log("An error occured"));
};
fetchBlogs();
}, []);
// the Provider gives access to the context to its children
return <Context.Provider value={code}>{children}</Context.Provider>;
};
export const useCoder = () => useContext(Context);
export default Provider;
What the issue could be here?
Thank you in advance for help:)

Fetch data from api in createContext using Fetch api in React doesn't work

I have an existing context for products. Where initially I used some mock data as shown below STORE_DATA to render the components. Now I need to replace that mock data and connect to a Node.js api which is available on my local port (created the api I after I created the react-app).
import React, { createContext, useState } from 'react';
import STORE_DATA from '../shop';
export const ProductsContext = createContext();
const ProductsContextProvider = ({ children }) => {
const [products] = useState(STORE_DATA);
return (
<ProductsContext.Provider value={{ products }}>
{
children
}
</ProductsContext.Provider>
);
}
export default ProductsContextProvider;
Just created a helper.js file witht he following to fetch the data:
import {useEffect} from "react";
const fetchData = () => {
return fetch("https://localhost:8081/products") <<tested on postman and works fine.
.then((response) => response.json())
.then((data) => console.log('Fetching Data:',data));
}
How to replace the mock data on the context file and use this fetchData() using useEffect within the context? What code should change?
Tried the following, but didn't work, can't even print the console.log:
import React, { createContext, useState, useEffect } from 'react';
import { fetchData } from '../helpers';
export const ProductsContext = createContext();
const ProductsContextProvider = ({ children }) => {
const [products, setProducts] = useState(null);
useEffect(() => {
setProducts(fetchData());
}, []);
return (
<ProductsContext.Provider value={{ products }}>
{
children
}
</ProductsContext.Provider>
);
}
export default ProductsContextProvider;
The issue was that it was returning the following error (explained):
net::ERR_SSL_PROTOCOL_ERROR (on chrome)
Solution: Use http:// instead of https:// in the URL's in the following code:
const fetchData = () => {
return fetch("http://localhost:8081/products")
.then((response) => response.json())
.then((data) => console.log('Fetching Data:',data));
}

React Hooks + Mobx => Invalid hook call. Hooks can only be called inside of the body of a function component

I have a React Native App,
Here i use mobx ("mobx-react": "^6.1.8") and react hooks.
i get the error:
Invalid hook call. Hooks can only be called inside of the body of a function component
Stores index.js
import { useContext } from "react";
import UserStore from "./UserStore";
import SettingsStore from "./SettingsStore";
const useStore = () => {
return {
UserStore: useContext(UserStore),
SettingsStore: useContext(SettingsStore),
};
};
export default useStore;
helper.js OLD
import React from "react";
import useStores from "../stores";
export const useLoadAsyncProfileDependencies = userID => {
const { ExamsStore, UserStore, CTAStore, AnswersStore } = useStores();
const [user, setUser] = useState({});
const [ctas, setCtas] = useState([]);
const [answers, setAnswers] = useState([]);
useEffect(() => {
if (userID) {
(async () => {
const user = await UserStore.initUser();
UserStore.user = user;
setUser(user);
})();
(async () => {
const ctas = await CTAStore.getAllCTAS(userID);
CTAStore.ctas = ctas;
setCtas(ctas);
})();
(async () => {
const answers = await AnswersStore.getAllAnswers(userID);
UserStore.user.answers = answers.items;
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
setAnswers(answers.items);
})();
}
}, [userID]);
};
Screen
import React, { useEffect, useState, useRef } from "react";
import {
View,
Dimensions,
SafeAreaView,
ScrollView,
StyleSheet
} from "react-native";
import {
widthPercentageToDP as wp,
heightPercentageToDP as hp
} from "react-native-responsive-screen";
import { observer } from "mobx-react";
import useStores from "../../stores";
import { useLoadAsyncProfileDependencies } from "../../helper/app";
const windowWidth = Dimensions.get("window").width;
export default observer(({ navigation }) => {
const {
UserStore,
ExamsStore,
CTAStore,
InternetConnectionStore
} = useStores();
const scrollViewRef = useRef();
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
if (InternetConnectionStore.isOffline) {
return;
}
Tracking.trackEvent("opensScreen", { name: "Challenges" });
useLoadAsyncProfileDependencies(UserStore.userID);
}, []);
React.useEffect(() => {
const unsubscribe = navigation.addListener("focus", () => {
CTAStore.popBadget(BadgetNames.ChallengesTab);
});
return unsubscribe;
}, [navigation]);
async function refresh() {
const user = await UserStore.initUser(); //wird das gebarucht?
useLoadAsyncProfileDependencies(UserStore.userID);
if (user) {
InternetConnectionStore.isOffline = false;
}
}
const name = UserStore.name;
return (
<SafeAreaView style={styles.container} forceInset={{ top: "always" }}>
</SafeAreaView>
);
});
so now, when i call the useLoadAsyncProfileDependencies function, i get this error.
The Problem is that i call useStores in helper.js
so when i pass the Stores from the Screen to the helper it is working.
export const loadAsyncProfileDependencies = async ({
ExamsStore,
UserStore,
CTAStore,
AnswersStore
}) => {
const userID = UserStore.userID;
if (userID) {
UserStore.initUser().then(user => {
UserStore.user = user;
});
CTAStore.getAllCTAS(userID).then(ctas => {
console.log("test", ctas);
CTAStore.ctas = ctas;
});
AnswersStore.getAllAnswers(userID).then(answers => {
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
});
}
};
Is there a better way? instead passing the Stores.
So that i can use this function in functions?
As the error says, you can only use hooks inside the root of a functional component, and your useLoadAsyncProfileDependencies is technically a custom hook so you cant use it inside a class component.
https://reactjs.org/warnings/invalid-hook-call-warning.html
EDIT: Well after showing the code for app.js, as mentioned, hook calls can only be done top level from a function component or the root of a custom hook. You need to rewire your code to use custom hooks.
SEE THIS: https://reactjs.org/docs/hooks-rules.html
You should return the value for _handleAppStateChange so your useEffect's the value as a depdendency in your root component would work properly as intended which is should run only if value has changed. You also need to rewrite that as a custom hook so you can call hooks inside.
doTasksEveryTimeWhenAppWillOpenFromBackgorund and doTasksEveryTimeWhenAppGoesToBackgorund should also be written as a custom hook so you can call useLoadAsyncProfileDependencies inside.
write those hooks in a functional way so you are isolating specific tasks and chain hooks as you wish without violiating the rules of hooks. Something like this:
const useGetMyData = (params) => {
const [data, setData] = useState()
useEffect(() => {
(async () => {
const apiData = await myApiCall(params)
setData(apiData)
})()
}, [params])
return data
}
Then you can call that custom hook as you wish without violation like:
const useShouldGetData = (should, params) => {
if (should) {
return useGetMyData()
}
return null
}
const myApp = () => {
const myData = useShouldGetData(true, {id: 1})
return (
<div>
{JSON.stringify(myData)}
</div>
)
}

How to perform a side effect after another side effect rendered a react component

I have the following simple use case:
The component ServerRendered is used to render markup that is retrieved from a server using the property url . In ServerRendered I use useEffect to load the markup from the back-end, set the state to the current markup and render it in a div.
The property init optionally specifies a function that should be executed after having rendered the markup.
How would I run the init function after the markup has been rendered?
/* eslint-disable react/no-danger */
import React, {useState, useEffect} from 'react';
import axios from 'axios';
type ServerRenderedPropsType = {
url: string,
init?: () => void,
};
function ServerRendered(props: ServerRenderedPropsType) {
const [html, setHtml] = useState('');
useEffect(() => {
async function fetchData() {
const result = await axios(props.url);
setHtml(result.data.title);
}
fetchData();
}, [props.url]);
return <div dangerouslySetInnerHTML={{__html: html}} className="serverRendered" />;
}
export default ServerRendered;
You have just to do another effet when you receive your data
import React, {useState, useEffect, useCallback} from 'react';
import axios from 'axios';
type ServerRenderedPropsType = {
url: string,
init?: () => void,
};
function ServerRendered(props: ServerRenderedPropsType) {
const [html, setHtml] = useState('');
const fetchData = useCallback(async () => {
const result = await axios(props.url);
setHtml(result.data.title);
}, [props.url])
useEffect(() => {
fetchData();
}, [fetchData]);
useEffect(() => {
// do what you want here
if (!html) return;
props.init()
}, [html])
return <div dangerouslySetInnerHTML={{__html: html}} className="serverRendered" />;
}
export default ServerRendered;

How to test a component which is using a custom TypeScript React Hook?

I'm currently writing a React component in Typescript which makes use of a axios-hooks hook called useAxios. An example of this hook in use is here:
export const App = () => {
const [{ data, loading, error }, refetch] = useAxios(
"https://api.myjson.com/bins/820fc"
);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
return (
<div>
<button onClick={e => refetch()}>refetch</button>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
const rootElement = document.getElementById("root");
render(<App />, rootElement);
I'm trying to figure out how to write a test where I can mock the useAxios hook. I've tried creating a mock of the underlying axios component but I cant get this working:
import React from "react"
import { render } from "#testing-library/react"
import { Test } from "../test"
import useAxios from "axios-hooks"
jest.mock("axios-hooks")
const mockedAxios = useAxios as jest.Mocked<typeof useAxios>
it("Displays loading", () => {
// How to mock the return values in the following line?
// mockedAxios.
const { getByText } = render(<Test />)
expect(getByText("Loading...")).toBeDefined()
})
I know that I shouldn't have to mock axios which is the underlying dependency, I should be able to mock useAxios, but I though I'd try anyhow.
I realise that this question has been mentioned many times on SO, but I can find a solution to this particular use case.
Any help greatly appreciated!
Mock the module and setup the expected result of useAxios per test e.g.
jest.mock('axios-hooks');
import useAxios from 'axios-hooks';
test('App displays loading when request is fetching', () => {
useAxios.mockReturnValue(Promise.resolve({ loading: true }));
// mount component
// Verify "Loading" message is rendered
});
I figured out how to do this myself. To test the custom hook I did the following:
import * as useAxios from "axios-hooks"
jest.mock("axios-hooks")
const mockedAxios = useAxios as jest.Mocked<typeof useAxios>
it("Displays loading message", async () => {
// Explicitly define what should be returned
mockedAxios.default.mockImplementation(() => [
{
data: [],
loading: true,
error: undefined
},
() => undefined
])
const { getByText } = render(<Test />)
expect(getByText("Loading...")).toBeDefined()
})
I had to jump through some additional hoops to get the compiler happy with the () => undefined parameter. I'm not a fan of the double as, but I'm not sure how to make it less verbose as I'm new to TS.
import * as useAxios from 'axios-hooks';
import { AxiosPromise } from 'axios';
import React from 'react';
import Test from 'components/Test';
import { render } from '#testing-library/react';
jest.mock('axios-hooks');
const mockedUseAxios = useAxios as jest.Mocked<typeof useAxios>;
it('renders a loading message', async () => {
mockedUseAxios.default.mockImplementation(() => [
{
data: [],
loading: true,
error: undefined,
},
(): AxiosPromise => (undefined as unknown) as AxiosPromise<unknown>,
]);
const { getByText } = render(<Test />);
expect(getByText('Loading...')).toBeDefined();
});

Resources