React HOC Using useEffect() - reactjs

So i am using a HOC for general error handling purposes in react like this:
import React, { useState, useEffect } from 'react'
import Modal from '../../UI/Modal/Modal'
const WithErrorHandler = (WrappedComponent, axios) => {
const NewComponent = props => {
console.log('UseState')
const [error, setError] = useState(null)
console.log('runs')
useEffect(() => {
const req = axios.interceptors.request.use(config => {
console.log('request intercepted')
return config
})
const res = axios.interceptors.response.use(null, error => {
setError(error)
return Promise.reject(error)
})
return () => {
axios.interceptors.request.eject(req)
axios.interceptors.response.eject(res)
}
}, [])
return (
<div>
{console.log('render')}
{error ? (
<Modal clickHandler={() => setError(null)}> {error.message}</Modal>
) : null}
<WrappedComponent {...props} />
</div>
)
}
return NewComponent
}
export default WithErrorHandler
The problem i have run into is that i have a component which fires an axios request in it's useEffect().
When i try to wrap this component with my WithErrorHandler the useEffect of the wrapped component fires first then the useEffect of HOC withErrorHandler runs. This causes the axios request to be made faster than the HOC could register the axios interceptors. Any ideas on how to fix this would be aprreciated.

You can define an intermediate state which prevents from rendering wrapped component.
const WithErrorHandler = (WrappedComponent, axios) => {
const NewComponent = (props) => {
const [ready, setReady] = useState(false); // HERE
console.log("UseState");
const [error, setError] = useState(null);
console.log("runs");
useEffect(() => {
const req = axios.interceptors.request.use((config) => {
console.log("request intercepted");
return config;
});
const res = axios.interceptors.response.use(null, (error) => {
setError(error);
return Promise.reject(error);
});
setReady(true); // HERE
return () => {
axios.interceptors.request.eject(req);
axios.interceptors.response.eject(res);
};
}, []);
if (!ready) return null; // HERE
return (
<div>
{console.log("render")}
{error ? (
<Modal clickHandler={() => setError(null)}> {error.message}</Modal>
) : null}
<WrappedComponent {...props} />
</div>
);
};
return NewComponent;
};
What it does is that it makes sure that axios interceptor is initialized and it is good to render wrapped component.
Instead of if (!ready) return null; you can return a more sensible state from your HOC for instance, if (!ready) return <p>Initializing...</p>

You need an extra render for the NewComponent callback to run, adding a conditional rendering on WrappedComponent should do the trick.
Notice that we set isFirstRender on promise success, change it dependenly on your use case.
const WithErrorHandler = (WrappedComponent, axios) => {
const NewComponent = (props) => {
const [isFirstRender, setIsFirstRender] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (isFirstRender) {
const req = axios.interceptors.request.use((config) => {
return config;
});
// Check req success
if (req.isSuccess) { setIsFirstRender(false); }
const res = axios.interceptors.response.use(null, (error) => {
setError(error);
return Promise.reject(error);
});
return () => {
axios.interceptors.request.eject(req);
axios.interceptors.response.eject(res);
};
}
}, [isFirstRender]);
return (
<div>
{error ? (
<Modal clickHandler={() => setError(null)}> {error.message}</Modal>
) : null}
{!isFirstRender && <WrappedComponent {...props} />}
</div>
);
};
return NewComponent;
};

Related

Need to call an alert message component from action in react

I've created a common component and exported it, i need to call that component in action based on the result from API. If the api success that alert message component will call with a message as "updated successfully". error then show with an error message.
calling service method in action. is there any way we can do like this? is it possible to call a component in action
You have many options.
1. Redux
If you are a fan of Redux, or your project already use Redux, you might want to do it like this.
First declare the slice, provider and hook
const CommonAlertSlice = createSlice({
name: 'CommonAlert',
initialState : {
error: undefined
},
reducers: {
setError(state, action: PayloadAction<string>) {
state.error = action.payload;
},
clearError(state) {
state.error = undefined;
},
}
});
export const CommonAlertProvider: React.FC = ({children}) => {
const error = useSelector(state => state['CommonAlert'].error);
const dispatch = useDispatch();
return <>
<MyAlert
visible={error !== undefined}
body={error} onDismiss={() =>
dispatch(CommonAlertSlice.actions.clearError())} />
{children}
</>
}
export const useCommonAlert = () => {
const dispatch = useDispatch();
return {
setError: (error: string) => dispatch(CommonAlertSlice.actions.setError(error)),
}
}
And then use it like this.
const App: React.FC = () => {
return <CommonAlertProvider>
<YourComponent />
</CommonAlertProvider>
}
const YourComponent: React.FC = () => {
const { setError } = useCommonAlert();
useEffect(() => {
callYourApi()
.then(...)
.catch(err => {
setError(err.message);
});
});
return <> ... </>
}
2. React Context
If you like the built-in React Context, you can make it more simpler like this.
const CommonAlertContext = createContext({
setError: (error: string) => {}
});
export const CommonAlertProvider: React.FC = ({children}) => {
const [error, setError] = useState<string>();
return <CommonAlertContext.Provider value={{
setError
}}>
<MyAlert
visible={error !== undefined}
body={error} onDismiss={() => setError(undefined)} />
{children}
</CommonAlertContext.Provider>
}
export const useCommonAlert = () => useContext(CommonAlertContext);
And then use it the exact same way as in the Redux example.
3. A Hook Providing a Render Method
This option is the simplest.
export const useAlert = () => {
const [error, setError] = useState<string>();
return {
setError,
renderAlert: () => {
return <MyAlert
visible={error !== undefined}
body={error} onDismiss={() => setError(undefined)} />
}
}
}
Use it.
const YourComponent: React.FC = () => {
const { setError, renderAlert } = useAlert();
useEffect(() => {
callYourApi()
.then(...)
.catch(err => {
setError(err.message);
});
});
return <>
{renderAlert()}
...
</>
}
I saw the similar solution in Antd library, it was implemented like that
codesandbox link
App.js
import "./styles.css";
import alert from "./alert";
export default function App() {
const handleClick = () => {
alert();
};
return (
<div className="App">
<button onClick={handleClick}>Show alert</button>
</div>
);
}
alert function
import ReactDOM from "react-dom";
import { rootElement } from ".";
import Modal from "./Modal";
export default function alert() {
const modalEl = document.createElement("div");
rootElement.appendChild(modalEl);
function destroy() {
rootElement.removeChild(modalEl);
}
function render() {
ReactDOM.render(<Modal destroy={destroy} />, modalEl);
}
render();
}
Your modal component
import { useEffect } from "react";
export default function Modal({ destroy }) {
useEffect(() => {
return () => {
destroy();
};
}, [destroy]);
return (
<div>
Your alert <button onClick={destroy}>Close</button>
</div>
);
}
You can't call a Component in action, but you can use state for call a Component in render, using conditional rendering or state of Alert Component such as isShow.

React Hooks setState function is not a function error

I'm getting this error in React Hooks. The function exists but every time I type something in to the search bar I get this TypeError.
TypeError : setSearchField is not a function
Here's the code for reference :
export default function StudentAPI() {
const [searchField, setSearchField] = ('');
const [students, setStudents] = useState([]);
const getStudents = async () => {
return axios
.get("https://api.hatchways.io/assessment/students")
.then((res) => {
setStudents(res.data.students);
})
.catch((err) => console.log(err));
};
useEffect(() => {
getStudents();
}, []);
const handleChange = (e) => {
setSearchField(e.target.value);
}
const filteredStudents = students.filter((student) => {
console.log(student.firstName);
// return student.firstName.toLowerCase().includes(search.toLowerCase()) ||
// student.lastName.toLowerCase().includes(search.toLowerCase());
})
return (
<div className="container">
<SearchBox
placeholder={'Search by name'}
handleChange={handleChange}
value={searchField}
/>
{filteredStudents.length > 0 ? filteredStudents.map((student) => {
return <Student key={student.id} student={student}/>;
}) : students.map((student) => {
return <Student key={student.id} student={student}/>;
})}
</div>
);
};
You have to use the hook useState
const [searchField, setSearchField] = usestate('');
You must have the state declaration above
const [searchField,setSearchField]=useState()
You have an error because useState is not written!
You must change
const [searchField, setSearchField] = ('');
to
const [searchField, setSearchField] = useState('');

React Hooks Sending message at first in Websockets

I can't send message after subsequential clicks, if i click button at first time it is sending message to server, after that it is not sending messages to server.
import { useEffect, useState, useRef } from "react";
import Header from "../src/Components/Header";
import ChatHistory from "../src/Components/ChatHistory";
import ChatArea from "../src/Components/ChatArea";
function App() {
const [messages, setMessages] = useState([]);
const testValue = { messages, setMessages };
const socket = useRef(null);
const renderCount = useRef(0);
const sendMessage = (msg = "test") => {
if (socket.current) {
socket.current.send(msg);
}
addMessages(msg);
};
const addMessages = (msg) => {
setMessages((prev) => [...prev, msg]);
};
useEffect(() => {
socket.current = new WebSocket("ws://localhost:8001/ws");
socket.current.onmessage = (msg) => {
addMessages(msg);
};
}, []);
useEffect(() => {
return () => {
if (socket.current) {
socket.current.close();
}
};
}, [socket]);
console.log("i am rendering");
return (
<>
<Header />
<ChatHistory chatHistory={messages.current} />
<div>
<button onClick={sendMessage}>Send</button>
</div>
</>
);
}
export default App;
Above mentioned one is my code, While clicking send button at first time, it is triggering message to server, after another subsequential clicks it isn't triggering message to server. Help needed.
Your second useEffect actually closing the connection after the first render, it's unnecessary.
Moreover you don't really need to save your socket instance in a ref, usually you need a single instance:
const socket = new WebSocket("ws://localhost:8001/ws");
function App() {
const [messages, setMessages] = useState([]);
const addMessages = (msg) => {
setMessages((prev) => [...prev, msg]);
};
const sendMessage = (msg = "test") => {
socket.send(msg);
addMessages(msg);
};
// Setup
useEffect(() => {
socket.current.onmessage = addMessages;
}, []);
// Runs on App unmount, means on closing the application
useEffect(() => {
return () => {
socket.close();
};
}, []);
return (
<>
<Header />
<ChatHistory chatHistory={messages.current} />
<div>
<button onClick={sendMessage}>Send</button>
</div>
</>
);
}
useEffect(() => {
return () => {
if (socket.current) {
socket.current.close();
}
};
}, [socket]);
everytime the socket is changing, you close it
try to unmont in the same useEffect that is defining socket
useEffect(() => {
if (!socket.current) {
socket.current = new WebSocket("ws://localhost:8001/ws");
socket.current.onmessage = (msg) => {
addMessages(msg);
};
}
return () => {
if (socket.current) {
socket.current.close();
}
};
}, [socket]);
Note useRef is not optimal for that case, use useState instead
This is working, but i am not sure what i am did wrong.
import { useEffect, useRef, useState } from "react";
import Header from "./Components/Header";
import ChatHistory from "./Components/ChatHistory";
function App() {
const [chatHistory, setChatHistory] = useState([]);
const [isOnline, setIsOnline] = useState(false);
const [textValue, setTextValue] = useState("");
const webSocket = useRef(null);
webSocket.current = new WebSocket("ws://localhost:8001/ws");
useEffect(() => {
setTimeout(() => {
if (webSocket.current.readyState === WebSocket.OPEN) {
setIsOnline(true);
}
if (webSocket.current.readyState === WebSocket.CLOSED) {
setIsOnline(false);
setChatHistory([]);
}
}, 5);
}, [webSocket.current]);
const sendMessage = () => {
if (webSocket.current.readyState === WebSocket.OPEN) {
setChatHistory([...chatHistory, textValue]);
webSocket.current.send(textValue);
}
};
return (
<>
<div className="App">
<Header onLine={isOnline} />
<ChatHistory chatHistory={chatHistory} />
<input
type="text"
onChange={(e) => setTextValue(e.target.value)}
value={textValue}
placeholder="Type Message..."
/>
<button onClick={sendMessage}>Hit</button>
</div>
</>
);
}
export default App;

unable to use hooks in a component that is being returned

I am following along with a React.js tutorial.
In it, a website was created with class based components. But now, it's being converted to functional component.
This is a HOC, which was at first returning a class based component but now a functional one.
Now, when i use useState and useEffect in it, it gives the error that :
Line 8:36: React Hook "useState" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
and
Line 29:9: React Hook "useEffect" cannot be called inside a callback. React Hooks must be
called in a React function component or a custom React Hook function
i'm using react version 17, while the instructor was using version 16.
This is the code when it was class based and working:
import React, {Component} from 'react'
import Modal from "../../components/UI/Modal/Modal";
const WithErrorHandler = (WrappedComponent , axios) =>
{
return class extends Component
{
state = {
error : null
}
componentWillMount()
{
this.resInterceptor = axios.interceptors.response.use(res => res , (error) =>
{
this.setState({error : error});
});
this.reqInterceptor =axios.interceptors.request.use((req) =>
{
this.setState({error : null});
return(req);
} );
}
componentWillUnmount()
{
axios.interceptors.request.eject(this.reqInterceptor);
axios.interceptors.response.eject(this.resInterceptor);
}
errorConfirmedHandler = () =>
{
this.setState({error : null});
}
render()
{
return(
<>
<Modal
show = {this.state.error}
modalClosed = {this.errorConfirmedHandler}>
{this.state.error ? this.state.error.message : null}
</Modal>
<WrappedComponent {...this.props}/>
</>
);
}
}
}
export default WithErrorHandler;
and this is the code when it's converted to functional component and not working :
import React, {useState , useEffect} from 'react'
import Modal from "../../components/UI/Modal/Modal";
const WithErrorHandler = (WrappedComponent , axios) =>
{
return props => {
const [error , seterror] = useState(null);
const resInterceptor = axios.interceptors.response.use(res => res , (error) =>
{
seterror(error);
});
const reqInterceptor =axios.interceptors.request.use((req) =>
{
seterror(null);
return(req);
} );
useEffect(() =>
{
return () =>
{
axios.interceptors.request.eject(reqInterceptor);
axios.interceptors.response.eject(resInterceptor);
}
} , [reqInterceptor , resInterceptor])
const errorConfirmedHandler = () =>
{
seterror(null);
}
return(
<>
<Modal
show = {error}
modalClosed = {errorConfirmedHandler}>
{error ? error.message : null}
</Modal>
<WrappedComponent {...props}/>
</>
);
}
}
export default WithErrorHandler;
Any help or guidance will be appreciated, thankyou.
Do you need those effects to run inside the returned component? One solution is to move all the hooks outside into the Wrapping component:
import React, { useState, useEffect } from 'react'
import Modal from '../../components/UI/Modal/Modal'
const WithErrorHandler = (WrappedComponent, axios) => {
const [error, seterror] = useState(null)
useEffect(() => {
const resInterceptor = axios.interceptors.response.use(
(res) => res,
(error) => {
seterror(error)
},
)
const reqInterceptor = axios.interceptors.request.use((req) => {
seterror(null)
return req
})
return () => {
axios.interceptors.request.eject(reqInterceptor)
axios.interceptors.response.eject(resInterceptor)
}
}, [axios.interceptors.request, axios.interceptors.response])
return (props) => {
const errorConfirmedHandler = () => {
seterror(null)
}
return (
<>
<Modal show={error} modalClosed={errorConfirmedHandler}>
{error ? error.message : null}
</Modal>
<WrappedComponent {...props} />
</>
)
}
}
export default WithErrorHandler
Alternatively, you can turn that callback into its own component:
import React, { useState, useEffect } from 'react'
import Modal from '../../components/UI/Modal/Modal'
const WithErrorHandler = (WrappedComponent, axios) => (props) => (
<Wrapped {...props} axios={axios} WrappedComponent={WrappedComponent} />
)
const Wrapped = ({ axios, WrappedComponent, ...props }) => {
const [error, seterror] = useState(null)
const resInterceptor = axios.interceptors.response.use(
(res) => res,
(error) => {
seterror(error)
},
)
const reqInterceptor = axios.interceptors.request.use((req) => {
seterror(null)
return req
})
useEffect(() => {
return () => {
axios.interceptors.request.eject(reqInterceptor)
axios.interceptors.response.eject(resInterceptor)
}
}, [axios.interceptors.request, axios.interceptors.response, reqInterceptor, resInterceptor])
const errorConfirmedHandler = () => {
seterror(null)
}
return (
<>
<Modal show={error} modalClosed={errorConfirmedHandler}>
{error ? error.message : null}
</Modal>
<WrappedComponent {...props} />
</>
)
}
export default WithErrorHandler

Putting fetch function in a separate component

I'm trying to take out the fetchImages function from the following component and put it inside a new component:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import UnsplashImage from './UnsplashImage';
const Collage = () => {
const [images, setImages] = useState([]);
const [loaded, setIsLoaded] = useState(false);
const fetchImages = (count = 10) => {
const apiRoot = 'https://api.unsplash.com';
const accessKey =
'<API KEY>';
axios
.get(`${apiRoot}/photos/random?client_id=${accessKey}&count=${count}`)
.then(res => {
console.log(res);
setImages([...images, ...res.data]);
setIsLoaded(true);
});
};
useEffect(() => {
fetchImages();
}, []);
return (
<div className="image-grid">
{loaded
? images.map(image => (
<UnsplashImage
url={image.urls.regular}
key={image.id}
alt={image.description}
/>
))
: ''}
</div>
);
};
export default Collage;
For this, I created a new component called api.js, removed the entire fetchImage function from the above component and put it in to api.js like this:
api.js
const fetchImages = (count = 10) => {
const apiRoot = 'https://api.unsplash.com';
const accessKey =
'<API KEY>';
axios
.get(`${apiRoot}/photos/random?client_id=${accessKey}&count=${count}`)
.then(res => {
console.log(res);
setImages([...images, ...res.data]);
setIsLoaded(true);
});
};
export default fetchImages;
Next I took setIsLoaded(true); from api.js and paste it inside Collage component like this:
useEffect(() => {
fetchImages();
setIsLoaded(true);
}, []);
Now I can import fetchImages in to Collage component.
However, I don't know what should I do with this line inside the fetchImages function? This needs to go to Collage component, but res.data is not defined inside Collage component.
setImages([...images, ...res.data]);
How should I handle it?
There is many way to do that, but in your case.
You should use
const fetchImages = (afterComplete, count = 10) => {
const apiRoot = 'https://api.unsplash.com';
const accessKey = '<API KEY>';
axios
.get(`${apiRoot}/photos/random?client_id=${accessKey}&count=${count}`)
.then(res => {
console.log(res);
afterComplete(res.data);
});
};
export default fetchImages;
And in your Collage component:
const afterComplete = (resData) =>{
setImages([...images, ...resData]);
setIsLoaded(true);
}
useEffect(() => {
fetchImages(afterComplete);
}, []);
What you can do is create a custom hook ( sort of like a HOC)... Since I don't have an unsplash API key I'll give you an example with a different API but the idea is the same:
Here is your custom hook:
import { useState, useEffect } from 'react';
export const useFetch = url => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const fetchUser = async () => {
const response = await fetch(url);
const data = await response.json();
const [user] = data.results;
setData(user);
setLoading(false);
};
useEffect(() => {
fetchUser();
}, []);
return { data, loading };
};
Here is how you can use it in your component:
import { useFetch } from './api';
const App = () => {
const { data, loading } = useFetch('https://api.randomuser.me/');
return (
<div className="App">
{loading ? (
<div>Loading...</div>
) : (
<>
<div className="name">
{data.name.first} {data.name.last}
</div>
<img className="cropper" src={data.picture.large} alt="avatar" />
</>
)}
</div>
);
};
Here is a live demo: https://codesandbox.io/s/3ymnlq59xm

Resources