I need to to display an alert AFTER the promise from the route action fullfills, but I need to run it from the component which started the action.
Here is the half-working example. Please take a look.
https://codesandbox.io/s/eloquent-liskov-sloots?file=/src/test.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { TestFetcher } from "./test";
export default function App() {
const router = createBrowserRouter([
{
path: "/",
element: <TestFetcher />,
action: async () => {
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
return null;
}
}
]);
return <RouterProvider router={router} />;
}
import { useFetcher } from "react-router-dom";
export function TestFetcher() {
const fetcher = useFetcher();
return (
<>
<button onClick={click}>{fetcher.state}</button>
</>
);
function click() {
fetcher.submit({ test: "test" }, { method: "post", action: "/" });
// need to run alert from here
}
}
The action is a callback that is attached to the route, triggered when a form action is invoked, like submitting a form. The code will very likely need to wait for the action to complete and check the returned data that will be placed on fetcher.data. You can use the useEffect with a dependency on the form data to conditionally trigger the alert as an intentional side-effect.
Example:
export default function App() {
const router = createBrowserRouter([
{
path: "/",
element: <TestFetcher />,
action: async () => {
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
return /* return something to the calling component */;
}
}
]);
return <RouterProvider router={router} />;
}
import { useFetcher } from "react-router-dom";
export function TestFetcher() {
const fetcher = useFetcher();
useEffect(() => {
if (fetcher.data) {
// run alert from here
}
}, [fetcher.data]);
function click() {
fetcher.submit({ test: "test" }, { method: "post", action: "/" });
}
return (
<>
<button onClick={click}>{fetcher.state}</button>
</>
);
}
Related
Below is the code located at "Pages/home.js". // localhost:3000/home
import axios from 'axios';
import Section1 from '../components/home-sections/section-1';
const Homepage = ({ show }) => {
const Html = JSON.parse(show.response.DesktopHTML);
const renderSection = () => {
return Html.map((itemData,index)=>{
return(<div key={index}>{itemData.DisplayName}</div>)
})
}
return(
<div>
{ renderSection()}
<Section1 />
</div>
)
}
export const getServerSideProps = async ({ query }) => {
try {
const response = await axios.get(
`https://api.example.com/getHomeSection?title=Section 1`
);
return {
props: {
show: response.data,
},
};
} catch (error) {
return {
props: {
error: error.error,
},
};
}
};
export default Homepage;
Now same code I added into section-1.js and this file is located to "components/home-sections/section-1.js"
Now getServerSideProps is working fine in home.js, but in section-1.js it is not working.
Error: TypeError: show is undefined in section-1.js
You cannot use getServerSideProps in non-page components. You can either pass the prop from Home to HomeSection or create a context so the value can be available globally from the component tree
getServerSideProps can only be exported from a page. You can’t export
it from non-page files.
https://nextjs.org/docs/basic-features/data-fetching#only-allowed-in-a-page-2
getServerSideProps can only be exported from Page components. It will not be run on components imported into a page.
However, you could export a function from the component that returns the props, and call that function from the page's getServerSideProps function.
Create a getServerSideProps function on the component.
// #components/MyComponent.tsx
import { GetServerSidePropsContext } from 'next';
function MyComponent(props: IMyComponentProps) {
return (<div>MyComponent</div>;)
}
MyComponent.getServerSideProps = async (context: GetServerSidePropsContext): Promise<{ props: IMyComponentProps }> => {
return { props: { ... } };
}
export default MyComponent;
In your page's getServerSideProps function, call the component's getServerSideProps function and merge the props from the component with the props from the page.
// mypage.tsx
import MyComponent from '#components/MyComponent';
const Page: NextPageWithLayout = (props: IIndexPageProps) => {
return <MyComponent />;
}
export async function getServerSideProps(context: GetServerSidePropsContext): Promise<{ props: IIndexPageProps }> {
let componentServerSideProps = await MyComponent.getServerSideProps(context);
let otherServerSideProps = { props: { ... } };
return {
props: {
...componentServerSideProps.props,
...otherServerSideProps.props
}
};
}
I have the following react code in my project
import React from 'react';
import { Upload } from 'antd';
const { Dragger } = Upload;
...
<Dragger
accept={ACCEPTED_FORMATS}
beforeUpload={beforeUpload}
data-testid="upload-dragger"
maxCount={1}
onChange={({ file: { status } }) => {
if (status === 'done') onUploadComplete();
}}
progress={progress}
showUploadList={false}
>
{/* here i have a button, code ommited for clarity, if needed i'll post it */}
</Dragger>
And I want to test if the callback function onUploadComplete() was called when file.status is 'done'.
Here is how i am doing the test right now:
I have a jest.mock to simulate a dumb request that will always succeed
import React from 'react';
import { fireEvent, render, waitFor } from '#testing-library/react';
import { act } from 'react-dom/test-utils';
import { DraggerProps } from 'antd/lib/upload';
import userEvent from '#testing-library/user-event';
import UploadCompanyLogo from '../UploadCompanyLogo'; // This is the component where the dragger is placed
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
const { Upload } = antd;
const { Dragger } = Upload;
const MockedDragger = (props: DraggerProps) => {
console.log('log test');
return (
<Dragger
{...props}
action="greetings"
customRequest={({ onSuccess }) => {
setTimeout(() => {
onSuccess('ok');
}, 0);
}}
/>
);
};
return { ...antd, Upload: { ...Upload, Dragger: MockedDragger } };
});
The test itself (same file as the mock), where it renders the component (where antd will be imported), simulate an upload and then checks if the callback has been called.
it('completes the image upload', async () => {
const flushPromises = () => new Promise(setImmediate);
const { getByTestId } = render(elementRenderer({ onUploadComplete }));
const file = new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' });
const uploadDragger = await waitFor(() => getByTestId('upload-dragger'));
await act(async () => {
userEvent.upload(uploadDragger, file);
});
await flushPromises();
expect(onUploadComplete).toHaveBeenCalledTimes(1);
expect(onUploadComplete).toHaveBeenCalledWith();
});
elementRenderer
const elementRenderer = ({
onUploadComplete = () => {}
}) => (
<ApplicationProvider>
<UploadCompanyLogo
onUploadComplete={onUploadComplete}
/>
</ApplicationProvider>
);
ApplicationProvider
import React, { PropsWithChildren } from 'react';
import { ConfigProvider as AntdProvider } from 'antd';
import { RendererProvider as FelaProvider } from 'react-fela';
import { createRenderer } from 'fela';
import { I18nextProvider } from 'react-i18next';
import antdExternalContainer, {
EXTERNAL_CONTAINER_ID,
} from 'src/util/antdExternalContainer';
import { antdLocale } from 'src/util/locales';
import rendererConfig from 'src/fela/felaConfig';
import i18n from 'src/i18n';
const ApplicationProvider = (props: PropsWithChildren<{}>) => {
const { children } = props;
return (
<AntdProvider locale={antdLocale} getPopupContainer={antdExternalContainer}>
<FelaProvider renderer={createRenderer(rendererConfig)}>
<I18nextProvider i18n={i18n}>
<div className="antd-local">
<div id={EXTERNAL_CONTAINER_ID} />
{children}
</div>
</I18nextProvider>
</FelaProvider>
</AntdProvider>
);
};
export default ApplicationProvider;
This is currently not working, but have already been improved with the help of #diedu.
The console.log() I have put in the MockedDragger it's currently not showing. If I put a console.log() in both component and mockedDragger, it prints the component log.
Any tips on how to proceed?
I have already seen this issue and didn’t help.
The first thing you should change is the return you are doing in your mock. You are returning a new component called MockedDragger, not Dragger.
return { ...antd, Upload: { ...Upload, Dragger: MockedDragger } };
The next thing is the event firing. According to this issue you should use RTL user-event library and wrap the call in act
import userEvent from "#testing-library/user-event";
...
await act(async () => {
userEvent.upload(uploadDragger, file);
});
...
and finally, due to some asynchronous code running you need to flush the pending promises before checking the call
const flushPromises = () => new Promise(setImmediate);
...
await flushPromises();
expect(onUploadComplete).toHaveBeenCalled()
this is the full version
import { render, waitFor } from "#testing-library/react";
import App from "./App";
import userEvent from "#testing-library/user-event";
import { act } from "react-dom/test-utils";
import { DraggerProps } from "antd/lib/upload";
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
const { Upload } = antd;
const { Dragger } = Upload;
const MockedDragger = (props: DraggerProps) => {
return <Dragger {...props} customRequest={({ onSuccess }) => {
setTimeout(() => {
onSuccess('ok');
}, 0)
}} />;
};
return { ...antd, Upload: { ...Upload, Dragger: MockedDragger } };
});
it("completes the image upload", async () => {
const onUploadComplete = jest.fn();
const flushPromises = () => new Promise(setImmediate);
const { getByTestId } = render(
<App onUploadComplete={onUploadComplete} />
);
const file = new File(["(⌐□_□)"], "chucknorris.png", { type: "image/png" });
const uploadDragger = await waitFor(() => getByTestId("upload-dragger"));
await act(async () => {
userEvent.upload(uploadDragger, file);
});
await flushPromises();
expect(onUploadComplete).toHaveBeenCalled();
});
Unfortunately, I couldn't set up a codesandbox to show it's working, but let me know if you face any issue and I can push the code to a repo.
I want the function FetchAll.getAll() to be called from the AddTracker.addOne() method
Here's the UI component I'm exporting called FetchAll with the method called getAll
import React, { Component } from "react";
export default class FetchAll extends Component {
state = {
loading: true,
trackers: null,
};
getAll = async () => {
let res = await fetch("http://localhost:8181/trackers");
const json = await res.json();
this.setState({ trackers: json.data, loading: false });
};
componentDidMount() {
this.getAll();
}
render() {
return (
<>
{this.props.render(
this.state.loading,
this.state.trackers,
this.getAll
)}
</>
);
}
}
The Navbar Component that's importing FetchAll then passing in the getAll prop to the AssTracker component
import React from "react";
import { AddTracker, FetchAll } from "./index";
import { Navbar} from "react-bootstrap";
export default function NavComponent(props) {
return (
<>
<Navbar >
<FetchAll
render={(loading, trackers, getAll) => {
return <AddTracker getAll={getAll} />;
}}
/>
</Navbar>
</>
);
}
The AddTracker component where I want the props.getAll function to be called
import React, { useState } from "react";
import { Form, FormControl, Button } from "react-bootstrap";
const DB = "http://localhost:8181/trackers";
export default function AddTracker(props) {
function formClick(e) {
e.preventDefault();
let dataObj = { url: formData };
const addOne = async () => {
let res = await fetch(DB, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(dataObj),
}).catch((err) => console.log(err));
const text = await res.text();
console.log(text);
return text;
};
const runFirst = async () => {
await addOne();
props.getAll(); // nothing happens
console.log(props.getAll()); // Promise {<pending>}
console.log(props); // {getAll: f}
};
runFirst();
}
const [formData, setFormData] = useState();
return (
<>
<Form inline>
<FormControl
type="text"
onChange={(e) => {
setFormData(e.target.value);
}}
/>
<Button variant="primary" onClick={(e) => formClick(e)}>
Add Tracker
</Button>
</Form>
</>
);
}
I can not get props.getAll() to be called inside of AddTracker. I've tried calling it directly inside of addOne, I've tried extracting it to a constant in the outer scope per vscode. I'm not sure what else to try.I've got it working in another component which makes it all the more perplexing.
Any help would be appreciated.
Thanks!
Itʼs not clear from your question what problem you are encountering, but I notice that your addOne() function has a props parameter that shadows the props of the AddTracker component. So Iʼm guessing props.getAll is undefined where you try to call it because getAll() is a property of the outer (shadowed) props.
The eslint no-shadow rule is helpful for making this kind of problem more immediately obvious.
I am trying to migrate my previously working local state to redux. Now loading available Players works just fine, but deleting will somehow stop in the playerActions.js file, where I dispatch and then return an API Call. So to further give details here are my code parts in relevance:
PlayerPage.js (Component):
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { loadPlayers, deletePlayer } from '../../redux/actions/playerActions';
import PlayerForm from './playerform';
import PlayCard from './playercard';
import PropTypes from 'prop-types';
import { toast } from 'react-toastify';
class PlayerPage extends Component {
constructor(props) {
super(props);
this.handleDeletePlayer = this.handleDeletePlayer.bind(this);
state = {};
componentDidMount() {
const players = this.props;
players.loadPlayers().catch(err => {
alert('Loading players failed. ' + err);
});
}
handleDeletePlayer = player => {
toast.success('Player deleted');
try {
deletePlayer(player);
} catch (err) {
toast.error('Delete failed. ' + err.message, { autoClose: false });
}
};
render() {
const styles = {
margin: '20px'
};
return (
<div className="container-fluid">
<div>
<h2 style={styles}>Add Player</h2>
<div className="container-fluid">
<PlayerForm handleAddNewPlayer={this.handleAddPlayer} />
</div>
</div>
<hr></hr>
<div>
<h2 style={styles}>Available Player</h2>
<div className="container-fluid">
{this.props.players.map(player => (
<PlayCard
player={player}
key={player.id}
imageSource={`${process.env.API_URL}/${player.profileImg}`}
onDeletePlayer={this.handleDeletePlayer}
/>
))}
</div>
</div>
</div>
);
}
}
PlayerPage.propTypes = {
players: PropTypes.array.isRequired
};
function mapStateToProps(state) {
return {
players: state.players
};
}
const mapDispatchToProps = {
loadPlayers,
deletePlayer
};
export default connect(mapStateToProps, mapDispatchToProps)(PlayerPage);
And the Action being called is in here:
playerActions.js:
import * as types from './actionTypes';
import * as playerApi from '../../api/playerApi';
export function loadPlayersSuccess(players) {
return { type: types.LOAD_PLAYERS_SUCCESS, players };
}
export function deletePlayerOptimistic(player) {
return { type: types.DELETE_PLAYER_OPTIMISTIC, player };
}
export function loadPlayers() {
return function(dispatch) {
return playerApi
.getAllPlayers()
.then(players => {
dispatch(loadPlayersSuccess(players));
})
.catch(err => {
throw err;
});
};
}
export function deletePlayer(player) {
console.log('Hitting deletePlayer function in playerActions');
return function(dispatch) {
dispatch(deletePlayerOptimistic(player));
return playerApi.deletePlayer(player);
};
}
The console.log is the last thing the app is hitting. But the API Call is never made though.
API Call would be:
playerApi.js:
import { handleResponse, handleError } from './apiUtils';
const axios = require('axios');
export function getAllPlayers() {
return (
axios
.get(`${process.env.API_URL}/player`)
.then(handleResponse)
.catch(handleError)
);
}
export function deletePlayer(id) {
return (
axios
.delete(`${process.env.API_URL}/player/${id}`)
.then(handleResponse)
.catch(handleError)
);
}
I was like spraying out console.log in different places and files and the last one I am hitting is the one in playerActions.js. But after hitting it the part with return function(dispatch) {} will not be executed.
So if someone could point me in a general direction I'd be more than grateful.
It looks like you are calling your action creator deletePlayer but you aren't dispatching it correctly. This is why the console.log is being called but not the method that does the request.
I'd recommend taking a look at the documentation for mapDispatchToProps to fully understand how this works. In your example, you should just need to change the call to deletePlayer in your PlayerPage component to this.props.deletePlayer() to use the action creator after it's been bound to dispatch properly.
this how the mapDispatchToProps should be:
const mapDispatchToProps = dispatch => {
return {
load: () => dispatch(loadPlayers()),
delete: () => dispatch(deletePlayer()),
}
}
then call load players with this.props.load() and delete player with this.props.delete()
I have a page that generates a random number in getInitialProps() after 2 seconds. There's a button that allows the user to "refresh" the page via Router.push(). As getInitalProps() takes 2 seconds to complete, I'll like to display a loading indicator.
import React from 'react'
import Router from 'next/router'
export default class extends React.Component {
state = {
loading: false
}
static getInitialProps (context) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({random: Math.random()})
}, 2000)
})
}
render() {
return <div>
{
this.state.loading
? <div>Loading</div>
: <div>Your random number is {this.props.random}</div>
}
<button onClick={() => {
this.setState({loading: true})
Router.push({pathname: Router.pathname})
}}>Refresh</button>
</div>
}
}
How can I know when Router.push()/getInitialProps() completes so I can clear my loading indicator?
Edit: Using Router.on('routeChangeComplete') is the most obvious solution. However, there are multiple pages and the user could click on the button multiple times. Is there a safe way to use Router events for this?
Router.push() returns a Promise. So you can do something like...
Router.push("/off-cliff").then(() => {
// fly like an eagle, 'til I'm free
})
use can use Router event listener in pages/_app.js, manage page loading and inject state into component
import React from "react";
import App, { Container } from "next/app";
import Router from "next/router";
export default class MyApp extends App {
state = {
loading: false
};
componentDidMount(props) {
Router.events.on("routeChangeStart", () => {
this.setState({
loading: true
});
});
Router.events.on("routeChangeComplete", () => {
this.setState({
loading: false
});
});
}
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
}
render() {
const { Component, pageProps } = this.props;
return (
<Container>
{/* {this.state.loading && <div>Loading</div>} */}
<Component {...pageProps} loading={this.state.loading} />
</Container>
);
}
}
and you can access loading as a props in your page component.
import React from "react";
import Router from "next/router";
export default class extends React.Component {
static getInitialProps(context) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ random: Math.random() });
}, 2000);
});
}
render() {
return (
<div>
{this.props.loading ? <div>Loading</div> : <div>Your random number is {this.props.random}</div>}
<button
onClick={() => {
this.setState({ loading: true });
Router.push({ pathname: Router.pathname });
}}
>
Refresh
</button>
</div>
);
}
}
you can also show loading text in _app.js (I've commented), that way you don't have to check loading state in every pages
If you wanna use third-party package here a good one nprogress