I got this little piece of code with a button that navigate to another page :
export const Index = () => {
return (
<Box>
<h1>Some title</h1>
<Button component={Link} to={'/creation'}>Créer</Button>
</Box>
)
}
I would like to test that when I click on that button I actually navigate to my new page but I can't make the test work :
it("Should navigate to page 2 on button click", async() => {
render(<Index />, {wrapper: BrowserRouter})
const user = userEvent.setup()
expect(screen.getByText(/Créer/i)).toBeInTheDocument() // => this works
await user.click(screen.getByText(/Créer/i))
expect(screen.getByText(/Page 2/i)).toBeInTheDocument() // => doesn't work
})
For information : It works when I actually deploy my App
The problem is that Index does not contain any Routes.
Actually it's contained inside Routes in the App.tsx like that :
class Main extends Component {
render() {
return (
<ThemeProvider theme={MuiTheme}>
<BrowserRouter>
<AppRoutes/>
</BrowserRouter>
</ThemeProvider>
);
}
}
AppRoutes.tsx being :
const AppRoutes = () => {
return (
<Routes>
<Route index element={<Index/>} />
<Route path={"/creation"} element={<CreationIndex/>} />
</Routes>
)
}
In order to make the code works for test all I had to do was :
it("Should navigate to page 2 on button click", async() => {
render(<AppRoutes/>, {wrapper: BrowserRouter}); // => AppRoutes instead of Index
const user = userEvent.setup()
expect(screen.getByText(/Créer/i)).toBeInTheDocument()
await user.click(screen.getByText(/Créer/i))
expect(screen.getByText(/Page 2/i)).toBeInTheDocument()
})
Related
I'm build game interface that has the following user flow:
user lands on one of the games URL eg. www.name.com/game1, first gets intro screen, than game screen and finally fail or success screen.
I'm trying to figure out the most optimal way to do this. Bellow is the code that works just fine but I'm looking for more elegant and scale-able solution. Any idea?
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
// Import views and components
import Step1 from "../Intro/Step1";
import StatusBar from "../../components/StatusBar/StatusBar";
import Game1 from "./Games/Game1/Game1";
import Game2 from "./Games/Game2/Game2";
import Intro from "./Intro/Intro";
import Password from "./Password/Password";
import Success from "./Success/Success";
import Fail from "./Fail/Fail";
import FailBeginOnStart from "./Fail/FailBeginOnStart";
// Data
function Game() {
const data = {
game1: {
desc: "some description for game 1",
},
game2: {
desc: "some description for game 2",
},
};
// Get global states from redux toolkit
const showIntro = useSelector((state) => state.game.showIntro);
const showSuccess = useSelector((state) => state.game.showSuccess);
const showFail = useSelector((state) => state.game.showFail);
const showPassword = useSelector((state) => state.game.showPassword);
const completedGame = useSelector((state) => state.game.completedGame);
const selectedLanguage = useSelector((state) => state.game.selectedLanguage);
// Get current param from URL (example /game1)
const { game } = useParams();
// Strip slash to get matching game ID (example game1)
const gameId = game.replace(/-/g, "");
const GameScreen = () => {
// show intro screen
if (showIntro === true) {
return (
<>
<StatusBar />
<Intro path={game} id={gameId} data={data[gameId]} />
</>
);
}
// show success screen
if (showSuccess === true) {
return (
<>
<StatusBar />
<Success data={data[gameId]} />
</>
);
}
// show fail screen
if (showFail === true) {
return (
<>
<StatusBar />
<Fail data={data[gameId]} />
</>
);
}
// Show actual game
switch (true) {
case game === "game1":
return <Game1 data={data[gameId]} />;
case game === "game2":
return <Game2 data={data[gameId]} />;
default:
return <Step1 />;
}
};
return <GameScreen />;
}
export default Game;
I'd suggest React Router and changing to class based components for your use case. You'd do a BaseGame class as a template for the concrete games. (if you're using typescript you can make it an abstract class.).
These examples are without further information on the actual flow of your page so you might need to adjust it.
class BaseGame extends React.Component {
// creating dummy members that get overwritten in concrete game classes
desc = "";
id = 0;
data = {}
constructor(){
this.state={
intro: true,
success: false,
fail: false
}
}
/** just dummy functions as we don't have access to
/* abstract methods in regular javascript.
/*
*/
statusBar(){ return <div>Statusbar</div>}
gameScreen(){ return <div>the Game Screen</div>}
render(){
return (
{this.statusBar()}
{if(this.state.intro) <Intro data={this.data} onStart={() => this.setState({intro: false})}/>}
{if(this.state.success) <Success data={this.data}/>}
{if(this.state.fail) <Faildata={this.data}/>}
{if(!this.state.intro && !this.state.fail && !this.state.success) this.gameScreen()}
)
}
}
class Game1 extends BaseGame {
id = 1;
data = {GameData}
// actual implementation of the screens
statusBar(){ return <div>Game1 StatusBar</div>}
gameScreen(){ return (
<div>
<h1>The UI of Game 1</h1>
Make sure to setState success or failure at the end of the game loop
</div>
)}
}
and in your app function
const App = () => (
<Router>
<Switch>
<Route exact path="/" component={Step1} />
<Route path="/game1" component={Game1} />
<Route path="/game2" component={Game2} />
...
</Switch>
</Router>
)
just a quick idea. You propably want to add a centralized Store like Redux depending where you wanna manage the State. But the different Game Classes can very well manage it on their own when they don't need a shared state. Also depending what the statusBar does it can be outside of the game and just in your app function
...
<StatusBar />
<Router>
... etc.
</Router>
in your app function.
I have a react-router application with a header component that takes up the full height of the page when at the '/home' path. When I navigate to a new path, '/foo', I need the header's height to change.
When I load either page directly by typing the url into the browser, it has the correct height:
100vh for localhost:3000/home
15vh for localhost:3000/foo
However when I navigate from /home to /foo with the below code, the URL changes correctly, but the DOM does not re-render. The header still has a height of 100vh and the DOM does not contain the Foo component.
App.js
const App = () => {
return (
<Provider store={store}>
<Router>
<Header />
<Switch>
<Route path='/home' component={null}/>
<Route path='/foo' component={Foo}/>
</Switch>
</Router>
</Provider>
);
}
Home.js
const Home = () => {
// ... some more code
const navigate = useCallback(to => {
history.push(to);
}, []);
// ... some more code
};
Header.js
const Header = () => {
const { pathname } = useLocation();
const style = pathname === '/home'
? { height: '100vh'}
: { height: '15vh'}
return (
<div style={style}>
{ /* ...some more code */ }
</div>
);
};
Foo.js
const Foo = () => {
return (
<div>
Foo...
</div>
);
};
The header is not re-rendering because it is outside Switch. So, changing the route will not re-trigger the header.
You will have to import the header in the Home and Foo component so that it re-renders on changing the path.
I suspect the issue is that when changing the path it's not triggering a redraw.
I'd recommend using React-Routers 'Route' component within the header (assuming the extra div isn't an issue).
So it will look like this:
const HeaderInternal = () => {
return { /* ... some header code */ }
}
const Header = () => {
return (
<>
<Route path='/home'><div style={{ height: '100vh'}}><HeaderInternal /> </div></Route>
<Route path='/foo'><div style={{ height: '15vh'}}><HeaderInternal /> </div></Route>
</>
);
};
I haven't directly tested this, but it the Route component should enforce a redraw whenever you switch pages. HeaderInternal is just to be DRY, but if you had a single component, you can skip it.
I am trying to move the open state for material-ui dialog to redux to prevent it from closing when a rerender occurs, but i having trouble with the dialog when a rerender occurs. Although the state is saved in redux and the dialog does stay open whenever a rerender occurs the open state stays open but the dialog does show the open animation (fading in) which is kinda annoying.
Students.js (parent component of the modal)
const Students = ({
app: { studentsPage: { savedAddDialogOpen }},
setStudentsPageAddDialogOpen}) => {
// Create the local states
const [dialogOpen, setDialogOpen] = React.useState(savedAddDialogOpen);
const dialogOpenRef = React.useRef(savedAddDialogOpen);
// Change redux dialog open
const setReduxDialogState = () => {
setStudentsPageAddDialogOpen(dialogOpenRef.current, savedAddDialogOpen);
};
// Open add student dialog
const dialogClickOpen = () => {
setDialogOpen(true);
dialogOpenRef.current = true;
setTimeout(() => setReduxDialogState(), 300);
};
// Close add student dialog
const dialogClose = () => {
setDialogOpen(false);
dialogOpenRef.current = false;
setTimeout(() => setReduxDialogState(), 300);
};
return (
<Container>
{/* Add student modal */}
<AddStudentModal dialogOpen={dialogOpen} dialogClose={dialogClose} />
</Container>
)
}
// Set the state for this component to the global state
const mapStateToProps = (state) => ({
app: state.app,
});
AddStudentModal.js
const AddStudentModal = ({
dialogOpen, dialogClose
}) => {
return (
<Dialog
open={dialogOpen}
>
{/* Lots of stuff*/}
<DialogActions>
<Button onClick={dialogClose}>
Close dialog
</Button>
</DialogActions>
</Dialog>
)
};
I hope this should be sufficient. I tried checking if the open state is actually correct when a rerender occurs and it is correct every time but it looks like the dialog is closed at a rerender no matter what the open state is and only a few ms later actually notices that it should be opened.
Any help would be really appreciated
Edit 1: Found out it has nothing to do with the open state coming from redux, if i use open={true} it still flashes, so probably a problem with material-ui itself?
Edit 2: PrivateRoute.js
const PrivateRoute = ({
auth: { isAuthenticated, loadingAuth },
user: { loggedInUser },
component: Component,
roles,
path,
setLastPrivatePath,
...rest
}) => {
useEffect(() => {
if (path !== '/dashboard' && path !== '/profile') {
setLastPrivatePath(path);
}
// Prevent any useless errors with net line:
// eslint-disable-next-line
}, [path]);
// If we are loading the user show the preloader
if (loadingAuth) {
return <Preloader />;
}
// Return the component (depending on authentication)
return (
<Route
{...rest}
render={props =>
!isAuthenticated ? (
<Redirect to="/login" />
) : (loggedInUser && roles.some(r => loggedInUser.roles.includes(r))) ||
roles.includes('any') ? (
<Component {...props} />
) : (
<NotAuthorized />
)
}
/>
);
};
// Set the state for this component to the global state
const mapStateToProps = state => ({
auth: state.auth,
user: state.user
});
I found the problem thanks to #RyanCogswell!
For anyone having the same problem here is the cause for me and the fix:
I was passing components into the Route component like this:
<PrivateRoute
exact
path={'/dashboard/students'}
component={(props) => (
<Students {...props} selectedIndex={selectedIndexSecondary} />
)}
roles={['admin']}
/>
By doing it this way i could pass props through my privateRoute function but this would also happen if you send the component this way in a normal Route component
Solution for me is just moving selectedIndexSecondary to redux and sending the component the normal way it prevented the re-mounting.
So just doing it like this will prevent this from happening.
<PrivateRoute
exact
path={'/dashboard/students'}
component={Students}
roles={['admin']}
/>
Also this will solve the localstates in your components from resseting to the default value. So for me it fixed two problems!
i am newbie in react. i have a dropdown which gives me different language to select which i want to share to my all components but i couldn't do it.
so far i have tried localStorage and Context but whenever i change the value i cannot see the change in my App.js.
here's my Component
const RTL = () => {
const handleLanguageAction = lang => {
console.log(lang , 'language')
};
return (
<OptionsButton
list={portalLanguages}
float="right"
helper="Please select any language"
label="Language"
action={handleLanguageAction}
/>
);
};
here is my app.js
const App = ({ role }) => {
return (
<MuiThemeProvider theme={theme}>
<MuiPickersUtilsProvider utils={MomentUtils}>
<SnackbarProvider>
<BrowserRouter>
<div dir={DIR}>
<Layout>
<RouteFactory routes={RouteComponents} config={routesConfig} role={role} />
</Layout>
</div>
</BrowserRouter>
</SnackbarProvider>
</MuiPickersUtilsProvider>
</MuiThemeProvider>
);
};
i want app.js to listen the value change in dropdown function and also should presist the value even i refresh the page.
can someone help me? thanks
You can use a customEvent to communicate between RTL and App
RTL
const myEvent = new CustomEvent('language', { detail: { lang } });
document.body.dispatchEvent(myEvent);
App.js
document.body.addEventListener("language", (event) => {
console.log(event.detail);
});
I am trying to setup a website with a login screen for unauthorized users and a dashboard for authorized users using react router dom.
Every time there is a route change (dashboard routes) when a user clicks a link in the sidebar, for example. The useEffect inside dashboard component is called which fetches data that I already have.
## ROUTES ##
export const appRoutes = auth => [
{
path: '/',
component: () => auth ? <Redirect to='/dashboard' /> :<Login/>,
exact: true
},
{
path: '/dashboard',
component: Guilds ## REDIRECTS TO THE NEXT ROUTE WITH ID ##,
exact: true,
private: true
},
{
path: '/dashboard/:id',
component: Dashboard,
private: true
},
{
path: '/dashboard/*',
component: Dashboard,
private: true
}
]
export const dashboardRoutes = [
{
path: '/dashboard/:id',
component: Home,
exact: true
}
]
## SIMPLIFIED APP COMPONENT ##
export default function App() {
return (
<ThemeProvider theme={theme}>
<BrowserRouter>
<Switch>
{appRoutes(auth).map(value => {
if(value.private) return <PrivateRoute path={value.path} component={value.component} exact={value.exact} key={value.path} auth={auth} />;
else return <Route path={value.path} component={value.component} exact={value.exact} key={value.path} />;
})}
</Switch>
</BrowserRouter>
</ThemeProvider>
)
}
## SIMPLIFIED DASHBOARD COMPONENT ##
export default function Dashboard({ match }) {
const [guild, setGuild] = useState(null);
const [user, setUser] = useState(null);
useEffect(() => {
getGuild(match.params.id)
.then(res => {
setGuild(res.data);
return getUser();
})
.then(res => {
setUser(res.data);
})
.catch(err => {
console.log(err);
})
}, [match.params.id]);
return (
<div className={classes.root}>
<Header onToggleDrawer={onToggleDrawer} guild={guild} auth />
<SideBar onToggleDrawer={onToggleDrawer} isOpen={drawerOpen} user={user} />
<div className={classes.content}>
<div className={classes.toolbar} />
<div className={classes.contentContainer}>
{dashboardRoutes.map(value => {
return <Route exact={value.exact} path={value.path} component={value.component} key={value.path}/>
})}
</div>
</div>
</div>
)
}
## PRIVATE ROUTE COMPONENT ##
export const PrivateRoute = ({ component: Component, auth, ...rest }) => {
return (
<Route {...rest} render={(props) => (
auth
? <Component {...props} />
: <Redirect to='/' />
)} />
)
}
I'm not sure if I am approaching the situation correctly but any help would be great. I take it the function is called in-case a user comes to the site from a bookmark for example but if someone can shed some light that would be cool.
Thank you.
The reason behind that why the fetch is happening several times is the dependency array what you have for useEffect. I assume the match.params.id is changing when the user clicks then it changes the route which will trigger the fetch again.
Possible solutions:
1. Empty dependency array:
One possible solution can be if you would like to fetch only once your data is set the dependency array empty for useEffect. From the documentation:
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.
So if you have the following, it will run only once:
useEffect(() => {
// this part runs only once
}, []); // empty dependency array
2. Checking if the fetch happened already:
The other solution what I was thinking is to check if you have the value already in the guild variable just like below:
useEffect(() => {
// no value presented for guild
if (guild === null) {
// code which is running the fetch part
}
}, [match.params.id]);
I hope this gives you an idea and helps!