When should we use Error Boundary components? Only for missing props and stuff like that?
For example, imagine this api fetching hook:
const useFetch = () => {
...
const [error, setError] = useState(null);
const method = async () => {
try {
await api.fetchData();
} catch(err) {
setError(err);
}
};
useEffect(() => {
method();
},[]);
return { ..., error };
}
Now, in a component, I just do:
const MyComponent = () => {
const { error } = useFetch();
if (error) return <FallbackUI />;
return <MainUI />;
}
Can I use an ErrorBoundary component to handle this situation (api call errors) instead of conditionally rendering?
EDIT
And what about if I only want to display a fallback UI when my fetching data method fails and there any data was previously retrieved?
Something like:
const { data, getMoreData, error } = useFetchPosts(); // data is stateful inside the hook
if (error && !data) return <FallbackUI />;
return <MainUI data={data} />;
I've followed the following approach in my projects that are all hooks/functional component implementations.
I'm using https://github.com/bvaughn/react-error-boundary
import { ErrorBoundary } from "react-error-boundary";
<ErrorBoundary FallbackComponent={ErrorFallback}>
<MyComponent />
</ErrorBoundary>
//reject the promise so it gets bubbled up
const useFetch = () => {
...
const [error, setError] = useState(null);
const method = async () => {
try {
await api.fetchData();
} catch(err) {
// setError(err);
return Promise.reject(err);
}
};
useEffect(() => {
method();
},[]);
return { ..., error };
}
function ErrorFallback({ error }: { error: any }) {
return (
<>
// you custom ui you'd like to return
</>
);
}
EDIT:
I typically have this at the top level so this is typically a catch all for all the unhandled exceptions. In other words, I wrap my App.tsx in the root index.tsx file in an ErrorBoundary. So, my code looks more like this
...
<ErrorBoundary FallbackComponent={ErrorFallback}>
<SWRConfig ...>
<React.StrictMode>
<ScrollToTop></ScrollToTop>
<App ... />
</React.StrictMode>
</SWRConfig>
</ErrorBoundary>
Related
I have a layout component that is loaded once on App.jsx and stays the same throughout the session, but since the page is SSRed it loads first, then the layout displays after a second, is there a way to get the layout data along with the page without having to add it to every page?
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
const abortController = new AbortController();
const signal = abortController.signal;
const [content, setContent] = useState();
const router = useRouter();
console.log("url", router);
useEffect(() => {
const fetchUIContent = async () => {
let data;
let responseStatus;
try {
data = await axios.get("/api/layout", {
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
signal: signal,
});
responseStatus = data.status;
} catch (error) {
if (error.name === "AbortError") return;
console.log("Error message:", error.message);
} finally {
if (responseStatus == 200) {
setContent(await data.data);
} else {
console.log("Oops error", responseStatus, "occurred");
}
}
};
fetchUIContent();
return () => {
abortController.abort();
};
}, []);
return (
<Provider store={store}>
<SessionProvider session={session} /*option={{clientMaxAge: 10}}*/>
<ConfigProvider direction="ltr">
<HeadersComponent>
<Layout content={content}>
<Component {...pageProps} />
</Layout>
</HeadersComponent>
</ConfigProvider>
</SessionProvider>
</Provider>
);
}
I thought of using redux to get the layout data, but that would still need to make changes on each page and it is a pretty large project.
Fixed by https://github.com/vercel/next.js/discussions/39414
For Future readers, this is how to add extra props for use in MyApp
//to consume foo
function MyApp({ Component, pageProps, foo }) { //if you have {Component, pageProps: { session, ...pageProps }, foo} make sure foo is outside the pageProps destructure
return (
<MyFooConsumer foo={foo}>
<Component {...pageProps} />
</MyFooConsumer>
)
}
//to send foo
MyApp.getInitialProps = async (appContext) => {
// calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(appContext);
return { ...appProps, foo: 'bar' }
}
I have a small problem with act() error in react-testing-library. In useEffect I try to call an function that is a promise. Promise returns some data and displays it, but even if the tests pass, the act error is still displayed.
Component:
export function getUser() {
return Promise.resolve({ name: "Json" });
}
const Foo = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const loadUser = async () => {
const userData = await getUser();
setUser(userData);
};
loadUser();
}, []);
return (
<div>
<p>foo</p>
{user ? <p>User is: {user.name}</p> : <p>nothing</p>}
</div>
);
};
Also I have my Foo component inside of App Component, like that:
import Foo from "./components/Foo";
function App() {
return (
<div>
some value
<Foo />
</div>
);
}
export default App;
TEST:
test("should display userName", async () => {
render(<Foo />);
expect(screen.queryByText(/User is:/i)).toBeNull();
expect(await screen.findByText(/User is: JSON/i)).toBeInTheDocument();
});
Do You have any ideas to resolve it?
EDIT:
here's an error message
console.error
Warning: An update to Foo inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
Instead of act use waitFor to let the rendering resolve.
import { render, waitFor } from '#testing-library/react-native';
import { useEffect, useState } from 'react';
import { Text } from 'react-native';
describe('useState', () => {
it('useState', () => {
function MyComponent() {
const [foo] = useState('foo');
return <Text testID="asdf">{foo}</Text>;
}
const { getByTestId } = render(<MyComponent></MyComponent>)
expect(getByTestId("asdf").props.children).toBe("foo");
});
it('useState called async', async () => {
function MyComponent() {
const [foo, setFoo] = useState('foo');
useEffect(() => {
(async () => {
setFoo(await Promise.resolve('bar'))
})()
}, []);
return <Text testID="asdf">{foo}</Text>;
}
const {getByTestId} = await waitFor(()=>render(<MyComponent></MyComponent>))
expect(getByTestId("asdf").props.children).toBe("bar");
});
});
In addition to the above answer for situations where the update does not occur immediately, (i.e. with a timeout) you can use await act(()=>Promise.resolve()); (note this generates a warning due improper typing but the await is needed for it to work.
Here's an example with renderHook
it("should return undefined", async () => {
const { result } = renderHook(() => {
const [loaded, loadedFonts] = useExpoFonts([]);
return useReplaceWithNativeFontCallback(loaded, loadedFonts);
}, {});
const replaceWithNativeFont0 = result.current;
expect(replaceWithNativeFont0({})).toBeUndefined();
await act(() => Promise.resolve());
const replaceWithNativeFont1 = result.current;
expect(replaceWithNativeFont1({})).toBeUndefined();
});
And with the typical render
it("should render fonts", async () => {
function MyComponent() {
const [loaded, loadedFonts] = useExpoFonts([]);
const replaceWithNativeFont = useReplaceWithNativeFontCallback(
loaded,
loadedFonts
);
const style = replaceWithNativeFont({
fontFamily: "something",
fontWeight: "300",
});
return <View testID="blah" style={style} />;
}
const { unmount } = render(
<ThemeProvider>
<MyComponent />
</ThemeProvider>
);
await act(() => Promise.resolve());
expect(screen.getByTestId("blah").props.style).toStrictEqual({
fontFamily: "something",
fontWeight: "300",
});
unmount();
});
Im having troubles rendering components based on api calls in React. I fetch my data in useEffect hook update a state with the data. The state is null for a while before the api get all the data but by that time, the components are rendering with null values. This is what I have:
import React, { useEffect, useState } from 'react'
import axios from 'axios';
import { Redirect } from 'react-router-dom';
const Poll = (props) => {
const [poll, setPoll] = useState(null);
//if found is 0 not loaded, 1 is found, 2 is not found err
const [found, setFound] = useState(0);
useEffect(() => {
axios.get(`api/poll/${props.match.params.id}`)
.then(res => {
console.log(res.data);
setPoll(res.data);
setFound(1);
})
.catch(err => {
console.log(err.message);
setFound(2);
});
}, [])
if(found===2) {
return(
<Redirect to="/" push />
)
}else{
console.log(poll)
return (
<div>
</div>
)
}
}
export default Poll
That is my workaround but it doesnt feel like thats the way it should be done. How can I set it so that I wait for my api data to get back then render components accordingly?
You don't need to track the state of the API call like const [found, setFound] = useState(1). Just check if poll exists and also you can create a new state variable for tracking the error.
For example if (!poll) { return <div>Loading...</div>} this will render a div with 'loading...' when there is no data. See the code below, for complete solution,
import React, { useEffect, useState } from 'react'
import axios from 'axios';
import { Redirect } from 'react-router-dom';
const Poll = (props) => {
const [poll, setPoll] = useState(null);
const [hasError, setHasError] = useState(false);
useEffect(() => {
axios.get(`api/poll/${props.match.params.id}`)
.then(res => {
console.log(res.data);
setPoll(res.data);
})
.catch(err => {
console.log(err.message);
setHasError(true)
});
}, [])
if(!poll) {
console.log('data is still loading')
return(
<div>Loading....</div>
)
}
if (hasError) {
console.log('error when fetching data');
return (
<Redirect to="/" push />
)
}
return (
<div>
{
poll && <div>/* The JSX you want to display for the poll*/</div>
}
</div>
);
}
export default Poll
In your than, try to use a filter:
setPoll(poll.filter(poll => poll.id !== id));
Make sure to replace id by your identificator
The standard way is to have other variables for the loading and error states like this
const Poll = (props) => {
const [poll, setPoll] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
setLoading(true);
axios.get(`api/poll/${props.match.params.id}`)
.then(res => {
console.log(res.data);
setPoll(res.data);
})
.catch(err => {
console.log(err.message);
setError(true);
})
.finally(()=> {
setLoading(false);
};
}, [])
if(error) return <span>error<span/>
if(loading) return <span>loading<span/>
return (
<div>
// your poll data
</div>
)
}
guys i wanna convert this code:
export default class App extends Component {
constructor(props) {
super(props);
this.state = { isLoading: true };
}
performTimeConsumingTask = async () => {
return new Promise((resolve) =>
setTimeout(() => {
resolve('result');
}, 2000)
);
};
async componentDidMount() {
const data = await this.performTimeConsumingTask();
if (data !== null) this.setState({ isLoading: false });
}
render() {
if (this.state.isLoading) return <SplashScreen />;
const { state, navigate } = this.props.navigation;
return (something)
i wrote this code but it doesn`t work :
const App = () => {
const [fontLoaded, setFontLoaded] = useState(false);
const [isTimerOn, setIsTimerOn] = useState(true);
if (!fontLoaded) {
return (
<AppLoading
startAsync={fetchFonts}
onFinish={() => setFontLoaded(true)}
/>
);
}
useEffect(async () => {
const data = await performTimeConsumingTask();
if (data !== null) setIsTimerOn(false);
});
if (isTimerOn) return <SplashScreen />;
else {
return (something)
This will show an error :
Invariant Violation: Rendered More Hooks than during the previous render.
If I comment the useEffect hook it will run the splashScreen. Can any one help me in converting it?
Pass [] as an argument if you wanted to use this hook as componentDidMount
useEffect(async () => {
const data = await performTimeConsumingTask();
if (data !== null) setIsTimerOn(false);
}, []);
Here is a list of hooks how you can use hooks to replace lifecycle methods
https://medium.com/javascript-in-plain-english/lifecycle-methods-substitute-with-react-hooks-b173073052a
The Reason for getting an error is your component is rendering too many times and useEffect is also running on each render by passing [] will run the useEffect on first render as it will behave like componentDidMount.
Also follow this to make network calls inside useEffect
https://medium.com/javascript-in-plain-english/handling-api-calls-using-async-await-in-useeffect-hook-990fb4ae423
There must be no conditional return before using all the hooks, in your case you return before using useEffect.
Also useEffect must not run on every render since it sets state in your case. Since you only want it to run on initial render pass an empty array as the second argument.
Also useEffect callback function cannot be async.
Read more about useEffect hook in the documentation.
Check updated code below
const App = () => {
const [fontLoaded, setFontLoaded] = useState(false);
const [isTimerOn, setIsTimerOn] = useState(true);
const performTimeConsumingTask = async () => {
return new Promise((resolve) =>
setTimeout(() => {
resolve('result');
}, 2000)
);
};
useEffect(() => {
async function myFunction() {
const data = await performTimeConsumingTask();
if (data !== null) setIsTimerOn(false);
}
myFunction();
}, []); // With empty dependency it runs on initial render only like componentDidMount
if (!fontLoaded) {
return (
<AppLoading
startAsync={fetchFonts}
onFinish={() => setFontLoaded(true)}
/>
);
}
if (isTimerOn) return <SplashScreen />;
else {
return (something)
I'm newbie in React but I'm developing an app which loads some data from the server when user open the app. App.js render this AllEvents.js component:
const AllEvents = function ({ id, go, fetchedUser }) {
const [popout, setPopout] = useState(<ScreenSpinner className="preloader" size="large" />)
const [events, setEvents] = useState([])
const [searchQuery, setSearchQuery] = useState('')
const [pageNumber, setPageNumber] = useState(1)
useEvents(setEvents, setPopout) // get events on the main page
useSearchedEvents(setEvents, setPopout, searchQuery, pageNumber)
// for ajax pagination
const handleSearch = (searchQuery) => {
setSearchQuery(searchQuery)
setPageNumber(1)
}
return(
<Panel id={id}>
<PanelHeader>Events around you</PanelHeader>
<FixedLayout vertical="top">
<Search onChange={handleSearch} />
</FixedLayout>
{popout}
{
<List id="event-list">
{
events.length > 0
?
events.map((event, i) => <EventListItem key={event.id} id={event.id} title={event.title} />)
:
<InfoMessages type="no-events" />
}
</List>
}
</Panel>
)
}
export default AllEvents
useEvents() is a custom hook in EventServerHooks.js file. EventServerHooks is designed for incapsulating different ajax requests. (Like a helper file to make AllEvents.js cleaner) Here it is:
function useEvents(setEvents, setPopout) {
useEffect(() => {
axios.get("https://server.ru/events")
.then(
(response) => {
console.log(response)
console.log(new Date())
setEvents(response.data.data)
setPopout(null)
},
(error) => {
console.log('Error while getting events: ' + error)
}
)
}, [])
return null
}
function useSearchedEvents(setEvents, setPopout, searchQuery, pageNumber) {
useEffect(() => {
setPopout(<ScreenSpinner className="preloader" size="large" />)
let cancel
axios({
method: 'GET',
url: "https://server.ru/events",
params: {q: searchQuery, page: pageNumber},
cancelToken: new axios.CancelToken(c => cancel = c)
}).then(
(response) => {
setEvents(response.data)
setPopout(null)
},
(error) => {
console.log('Error while getting events: ' + error)
}
).catch(
e => {
if (axios.isCancel(e)) return
}
)
return () => cancel()
}, [searchQuery, pageNumber])
return null
}
export { useEvents, useSearchedEvents }
And here is the small component InfoMessages from the first code listing, which display message "No results" if events array is empty:
const InfoMessages = props => {
switch (props.type) {
case 'no-events':
{console.log(new Date())}
return <Div className="no-events">No results :(</Div>
default:
return ''
}
}
export default InfoMessages
So my problem is that events periodically loads and periodically don't after app opened. As you can see in the code I put console log in useEvents() and in InfoMessages so when it's displayed it looks like this:
logs if events are displayed, and the app itself
And if it's not displayed it looks like this: logs if events are not displayed, and the app itself
I must note that data from the server is loaded perfectly in both cases, so I have totally no idea why it behaves differently with the same code. What am I missing?
Do not pass a hook to a custom hook: custom hooks are supposed to be decoupled from a specific component and possibly reused. In addition, your custom hooks return always null and that's wrong. But your code is pretty easy to fix.
In your main component you can fetch data with a custom hook and also get the loading state like this, for example:
function Events () {
const [events, loadingEvents] = useEvents([])
return loadingEvents ? <EventsSpinner /> : <div>{events.map(e => <Event key={e.id} title={e.title} />}</div>
}
In your custom hook you should return the internal state. For example:
function useEvents(initialState) {
const [events, setEvents] = useState(initialState)
const [loading, setLoading] = useState(true)
useEffect(function() {
axios.get("https://server.ru/events")
.then(
(res) => {
setEvents(res.data)
setLoading(false)
}
)
}, [])
return [events, loading]
}
In this example, the custom hook returns an array because we need two values, but you could also return an object with two key/value pairs. Or a simple variable (for example only the events array, if you didn't want the loading state), then use it like this:
const events = useEvents([])
This is another example that you can use, creating a custom hook that performs the task of fetching the information
export const useFetch = (_url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(true);
useEffect(function() {
setLoading('procesando...');
setData(null);
setError(null);
const source = axios.CancelToken.source();
setTimeout( () => {
axios.get( _url,{cancelToken: source.token})
.then(
(res) => {
setLoading(false);
console.log(res.data);
//setData(res);
res.data && setData(res.data);
// res.content && setData(res.content);
})
.catch(err =>{
setLoading(false);
setError('si un error ocurre...');
})
},1000)
return ()=>{
source.cancel();
}
}, [_url])