I have a simple app that so far only has a main login component, and a home page with a navbar which the user is supposed to be routed to once they login to the app. The user experience should be as follows: user selects name from blue dropdown menu, once that name is selected, they click the green login button which dispatches setLoggedInUser from loggedInSlice.js and sets the selected user as the authorize object For some reason , my state from my redux store is lost when I wrap my login button in a router link tag which leads to the home page , but it is not lost when that login button doesnt link anywhere. I want to understand why I am not retaining my redux authUser state once I navigate from the userLogin.js component to the Main.js Component.
Components:
App.js
import logo from "./logo.svg";
import "./App.css";
import TestComponent from "./components/TestComponent";
import Main from "./components/Main";
import { Routes, Route, Link } from "react-router-dom";
import { useState } from "react";
import { Card, Button, Accordion, Dropdown } from "react-bootstrap";
import "bootstrap/dist/css/bootstrap.min.css";
import UserLogin from "./components/UserLogin";
import Nav from "./components/Nav";
function App() {
return (
<div className="App">
<Routes>
<Route path="home" element={<Main />} />
<Route path="/" element={<UserLogin />} />
<Route path="testcomponent" element={<TestComponent />} />
</Routes>
</div>
);
}
export default App;
UserLogin.js
import React from "react";
import { Routes, Route, Link, useNavigate } from "react-router-dom";
import { Redirect } from "react-router-dom";
import { useState, useEffect } from "react";
import { Card, Button, Accordion, Dropdown } from "react-bootstrap";
import { useSelector, useDispatch } from "react-redux";
import "bootstrap/dist/css/bootstrap.min.css";
import { setLoggedInUser } from "../features/loggedInSlice";
export default function UserLogin() {
let navigate = useNavigate();
const users = useSelector((state) => state.users);
const state = useSelector((state) => state);
const dispatch = useDispatch();
const [user, setUser] = useState("Select A User");
const [authorizedUser, setauthorizedUser] = useState({});
const handleSelect = (e) => {
setUser(e.target.text);
};
function authorizeUser() {
setauthorizedUser(
users.filter((userName) => userName.firstName === user)[0]
);
}
useEffect(() => {
dispatch(setLoggedInUser(authorizedUser));
}, [authorizedUser]);
function authorizeLogin() {
console.log(state);
}
console.log(state);
return (
<div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Card style={{ width: "50%" }}>
<Card.Header>Would you Rather?</Card.Header>
<Card.Body>
<Card.Title>Special title treatment</Card.Title>
<Card.Text>
With supporting text below as a natural lead-in to additional
content.
</Card.Text>
</Card.Body>
<Dropdown className="d-inline mx-2" onClick={handleSelect}>
<Dropdown.Toggle
id="dropdown-autoclose-true"
style={{ width: "60%" }}
>
{user}
</Dropdown.Toggle>
<Dropdown.Menu style={{ width: "60%" }}>
{users.map((user, index) => (
<Dropdown.Item href="#" key={index}>
{user.firstName}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
<div>
<Link to="home">
<Button
variant="success"
style={{ width: "20%", marginTop: "3%", marginBottom: "1%" }}
onClick={authorizeUser}
>
Login
</Button>
</Link>
</div>
</Card>
<button onClick={authorizeLogin}>click</button>
</div>
</div>
);
}
Main.js
import React from "react";
import NavBar from "./Nav";
import { Routes, Route, Link } from "react-router-dom";
export default function Main() {
return (
<div>
<NavBar />
</div>
);
}
Nav.js
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { Card, Button, Accordion, Dropdown, Nav } from "react-bootstrap";
export default function NavBar() {
const state = useSelector((state) => state);
console.log(state);
const loggedInFirstName = state.loggedIn.authUser.firstName;
return (
<div style={{ display: "flex" }}>
<Nav justify variant="tabs" defaultActiveKey="/home">
<Nav.Item>
<Nav.Link>Home</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link>New Poll </Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link>Leaderboard</Nav.Link>
</Nav.Item>
</Nav>
<h5 style={{ paddingLeft: "15px", paddingTop: "15px" }}>Welcome</h5>
</div>
);
}
Redux Store and Slices:
store.js
import { configureStore } from "#reduxjs/toolkit";
import usersReducer from "../features/usersSlice";
import loggedIn from "../features/loggedInSlice";
export const store = configureStore({
reducer: {
users: usersReducer,
loggedIn: loggedIn,
},
});
loggedInSlice.js
import React from "react";
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
authUser: [],
};
export const loggedIn = createSlice({
name: "loggedIn",
initialState,
reducers: {
setLoggedInUser: (state, action) => {
console.log(action.payload);
state.authUser = action.payload;
},
},
});
export default loggedIn.reducer;
export const { setLoggedInUser } = loggedIn.actions;
usersSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = [
{
firstName: "matt",
age: 30,
total: 0,
},
{
firstName: "mike",
age: 25,
total: 0,
},
{
firstName: "steve",
age: 22,
total: 0,
},
];
export const usersReducer = createSlice({
name: "UsersSlice",
initialState,
});
export default usersReducer.reducer;
To summarize I need to understand why my authUser is added to my redux store only when the login button does not route to another component
code that adds the authUser
<Button
variant="success"
style={{ width: "20%", marginTop: "3%", marginBottom: "1%" }}
onClick={authorizeUser}
>
Login
</Button>
code that does not add the authUser
<Link to="home">
<Button
variant="success"
style={{ width: "20%", marginTop: "3%", marginBottom: "1%" }}
onClick={authorizeUser}
>
Login
</Button>
</Link>
Issue
The issue here is that enqueued React state updates are asynchronously processed. The navigation action to "/home" when wrapping the login button with a Link occurs well before the enqueued state update is processed. React sees that the UserLogin component is no longer mounted and ignores (silently) the state update.
Solution
I suggest moving the "login" logic into an asynchronous action. This will return a Promise to the UserLogin component that can be waited on and upon successful authentication issue the navigation action to the "/home" path.
Example:
loggedInSlice:
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
const initialState = {
authUser: []
};
const loginUser = createAsyncThunk(
"loggedIn/loginHandler",
async (user, { dispatch }) => {
... asynchronous login logic ...
// if successfully logged in
dispatch(loggedIn.actions.setLoggedInUser(user));
return ... response object/success message/etc...;
}
);
export const loggedIn = createSlice({
name: "loggedIn",
initialState,
reducers: {
setLoggedInUser: (state, action) => {
state.authUser = action.payload;
}
}
});
export default loggedIn.reducer;
export const { setLoggedInUser } = loggedIn.actions;
export { loginUser };
UserLogin
Dispatch the login action directly in the button's onClick handler, wait for resolved Promise, then navigate.
...
import { loginUser } from "../features/loggedInSlice";
export default function UserLogin() {
const navigate = useNavigate();
const [user, setUser] = useState("Select A User");
const users = useSelector((state) => state.users);
const state = useSelector((state) => state);
const dispatch = useDispatch();
...
function authorizeUser() {
const selectedUser = users.find((userName) => userName.firstName === user);
dispatch(loginUser(selectedUser))
.unwrap()
.then((response) => {
console.log({ response });
navigate("/home");
});
}
...
return (
<div>
<div ...>
<Card ...>
...
<div>
<Button
...
onClick={authorizeUser}
>
Login
</Button>
</div>
</Card>
...
</div>
</div>
);
}
Related
I am having issues managing the state of my navbar using useContext. Atm my app renders the menu items as soon as the menu toggle. I want this event to happen only onClick, also the button does not log the console.log message, it works only when I click directly on the link item ex:home.
So I have 2 questions.
How do I manage my navbar state to show how to hide the menu items without having to create a new component for it?
How do I fix my click event for it be triggered on either the menu button itself or/and menu items?
Below you will code snippets for App.js, Layout.js, ThemeContext.js, useTheme.js, useToggle.js, ToggleContext.js and the Navbar where the toggle context is used.
I would really appreciate some help here guys, I am junior and really kind of stuck here.
Thanks in advance to you all.
App.js
//import { data } from '../../SkillData';
import Header from './Header';
import Navbar from './Navbar';
import Skills from './Skills';
import Layout from './Layout';
function App () {
return (
<Layout startingTheme="light" startingToggle={"show"}>
<div>
<Navbar />
<Header />
<Skills />
</div>
</Layout>
);
}
export default App;
Layout.js
import React, { useContext } from "react";
import { ThemeContext, ThemeProvider } from "../contexts/ThemeContext";
import { ToggleContext, ToggleProvider } from "../contexts/ToggleContext";
function Layout ({startingTheme, startingToggle, children}) {
return (
<>
<ThemeProvider startingTheme={startingTheme} >
<ToggleProvider startingToggle={startingToggle}>
<LayoutNoToggleProvider>
</LayoutNoToggleProvider>
</ToggleProvider>
<LayoutNoThemeProvider >{children}</LayoutNoThemeProvider>
</ThemeProvider>
</>
);
}
function LayoutNoToggleProvider ({children}) {
const toggle = useContext(ToggleContext);
return (
<div className={
toggle === false ? "navbar navbar-collapsed" : "navbar navbar-collapse show"
}>
{children}
</div>
)
}
function LayoutNoThemeProvider ({ children }) {
const {theme} = useContext(ThemeContext);
return (
<div className={
theme === "light" ?
"container-fluid bg-white" :
"container-fluid bg-dark"
}>
{children}
</div>
);
}
export default Layout;
ThemeContext
import React, { createContext} from "react";
import useTheme from "../hooks/useTheme";
export const ThemeContext = createContext();
function ThemeProvider ({children, startingTheme}) {
const { theme, setTheme } = useTheme(startingTheme);
return (
<ThemeContext.Provider value={
{theme, setTheme}
}>
{children}
</ThemeContext.Provider>
);
}
export { ThemeProvider };
useTheme.js
import { useState } from "react";
function useTheme (startingTheme ="light") {
const [theme, setTheme] = useState(startingTheme);
function validateTheme (themeValue) {
if (themeValue === "dark") {
setTheme("dark");
} else {
setTheme("light");
}
}
return {
theme,
setTheme: validateTheme,
}
}
export default useTheme;
ToggleContext.js
import React, { createContext } from "react";
import useToggle from "../hooks/useToggle";
export const ToggleContext = createContext();
function ToggleProvider({ children, startingToggle }) {
const { toggle, setToggle } = useToggle(startingToggle);
return (
<ToggleContext.Provider value={{ toggle, setToggle }}>
{children}
</ToggleContext.Provider>
);
}
export { ToggleProvider };
useToggle.js
import { useState } from "react";
function useToggle (startingToggle = false) {
const [toggle, setToggle] = useState(startingToggle);
function validateShowSidebar (showSidebarValue) {
if (showSidebarValue === "show") {
setToggle("show");
} else {
setToggle("");
}
}
return {
toggle,
setToggle: validateShowSidebar,
}
}
export default useToggle;
Navbar.js
import Image from "next/image";
import styles from "../../styles/Home.module.scss"
import Logo from "../../public/Knowledge Memo.svg"
import { useContext } from "react";
import { ThemeContext } from "../contexts/ThemeContext";
import { ToggleContext } from "../contexts/ToggleContext";
import Link from 'next/link';
import { useState } from "react";
const navbarData = [
{ id: "1",
title: "home",
ref: "#home"
},
{ id:"2",
title: "Skills",
ref: "#skills"
},
{ id:"3",
title: "The List",
ref: "#theList"
},
{ id: "4",
title: "Team",
ref: "#team"
},
{ id: "5",
title: "Contact",
ref: "#contact"
},
];
function Navbar() {
const theme = useContext(ThemeContext);
const toggle = useContext(ToggleContext);
return (
<>
<nav className={
theme === "light" ?
"navbar navbar-expand-lg navbar-dark fixed-top":
"navbar navbar-expand-lg navbar-dark bg-dark fixed-top id= mainNav"}>
<div className="container d-flex flex justify-content-between">
<a className="navbar-brand h-50" href="#page-top">
<div className="navbar-brand">
<Image
src={Logo}
alt="..."
fill="#fff"
objectFit="contain"
className="h-50"
/>
</div>
</a>
<button
onClick={ () => toggle === !toggle, console.log("clicked")}
className="navbar-toggler collapsed"
type="button"
data-bs-toggle="collapsed"
data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive"
aria-expanded="false"
aria-label="Toggle navigation"
>
Menu
<i className="fa fa-bars ms-1 navbar-toggler" aria-hidden="true"></i>
</button>
{toggle ?
<div className="collapsed navbar-collapse mt-2 id=navbarResponsive">
<ul className="navbar-nav text-uppercase ms-auto py-4 py-lg-0">
{navbarData.map((link,idx) => {
return (
<li key={link.id}>
<Link href={`/${link.ref}`} className="nav-item" data-index={idx} passHref>
<a className="nav-link">
{link.title}
</a>
</Link>
</li>
);
})}
</ul>
</div>
: <div className="collapse navbar-collapse show mt-2 id=navbarResponsive">
<ul className="navbar-nav show text-uppercase ms-auto py-4 py-lg-0">
{navbarData.map((link,idx) => {
return (
<li key={link.id}>
<Link href={`/${link.ref}`} className="nav-item" data-index={idx} passHref>
<a className="nav-link">
{link.title}
</a>
</Link>
</li>
);
})}
</ul>
</div>}
</div>
</nav>
</>
);
}
export default Navbar;
You can try out this implemetation with reducers to handle for you the state change with localstorage. It is not an exact implemetation of your's but you can see the flow
In the AppContext.jsx
The AppContext holds the global state of the application so that it's easier working with a single context provider and dispatching actons to specific reducers to handle state change without providing many providers.
The combinedReducers handle reducer methods to a given state component
import { useReducer, createContext, useEffect } from "react";
import userReducer from "./reducers/userReducer";
import themeReducer from "./reducers/themeReducer";
export const APP_NAME = "test_app";
//Check the localstorage or set a default state
const initialState = JSON.parse(localStorage.getItem(APP_NAME))
? JSON.parse(localStorage.getItem(APP_NAME))
: {
user: {
username: "",
email: "",
isAdmin: false,
},
theme: { dark: false },
};
//Create your global context
const AppContext = createContext(initialState);
//Create combined reducers
const combinedReducers = ({ user, theme }, action) => ({
user: userReducer(user, action),
theme: themeReducer(theme, action),
});
const AppState = ({ children }) => {
//Making it to provider state
const [state, dispatch] = useReducer(combinedReducers, initialState);
useEffect(() => {
localStorage.setItem(APP_NAME, JSON.stringify(state));
}, [state]);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
};
export default AppState;
export { AppContext, AppState };
The above implementation works like redux but you destructure the given state to a specific reducer to handle the state change
In this I have used localstorage to keep a persistent state because with context API on page reload the state goes. Use the useEffect hook from react and add the state in the dependency array to ensure your state is in sync
In the UserReducer.jsx
const userReducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case "LOGIN":
return { ...state, ...payload };
case "LOGOUT":
return {};
default:
return state;
}
};
export default userReducer;
In the ThemeReducer.jsx
const themeReducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case "DARK":
return { ...payload };
default:
return state;
}
};
export default themeReducer;
Wrapping the whole app with a single provider in the index.jsx
import reactDom from "react-dom"
import React from "react"
import App from "./App"
import "./index.css"
import AppState from "./state/AppState"
reactDom.render(
<React.StrictMode>
<AppState >
<App />
</AppState>
</React.StrictMode>,
document.getElementById("root")
)
Accessing the context from App.jsx
import { useContext } from "react";
import { AppContext } from "./state/AppState";
const App = () => {
const { state, dispatch } = useContext(AppContext);
const handleLogin = () => {
dispatch({
type: "LOGIN",
payload: {
username: "Mike",
email: "mike#gmail.com",
isAdmin: false,
},
});
};
const handleLogout = () => {
dispatch({
type: "LOGOUT",
payload: {},
});
};
return (
<div className="main-container">
<div className="container">
<p>Username: {state.user.username ? state.user.username : "Unknown"}</p>
<p>Email: {state.user.email ? state.user.email : "Unknown"}</p>
</div>
<button onClick={handleLogin}>Login</button>
<button onClick={handleLogout} style={{ background: "red" }}>
Login
</button>
</div>
);
};
export default App;
Here is my code LINK if you want to see the structure Github
I am trying to create a simple form app, where there will be a textarea input and a submit button. Where, if I type something in the textarea and then click submit, the text that I just typed will show under the button inside a tag. When im doing this without Redux, it works fine, even after when I use Redux partly meaning when I manage only one state (input field state) using Redux it works great. But when i make two reducers, and two dispatches then problem happens. Here are my codes.
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Provider from 'react-redux/es/components/Provider';
import {
createStore,
applyMiddleware,
combineReducers,
} from 'redux';
import { getInput, getOutput } from './reducer';
import { createLogger } from 'redux-logger';
import App from './App';
import reportWebVitals from './reportWebVitals';
const rootReducer = combineReducers({
getInput,
getOutput,
});
const logger = createLogger();
const store = createStore(
rootReducer,
applyMiddleware(logger)
);
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
reportWebVitals();
app.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
changeInput,
postOutput,
} from './action';
import {
Form,
Button,
Container,
} from 'react-bootstrap';
const mapStateToProps = (state) => {
return {
input: state.getInput.input,
output: state.getOutput.output,
};
};
const mapDispatchToProps = (dispatch) => {
return {
handleInput: (event) =>
dispatch(changeInput(event.target.value)),
handleClick: (props) =>
dispatch(postOutput(props.output)),
};
};
class App extends Component {
// constructor() {
// super();
// this.state = {
// output: '',
// };
// }
// handleInput = (event) => {
// this.setState({ input: event.target.value });
// };
// handleClick = () => {
// this.setState({
// output: this.props.input,
// });
// };
render() {
return (
<div>
<Container>
{' '}
<Form>
<Form.Group controlId='exampleForm.ControlTextarea1'>
<div>
<div
style={{
display: 'flex',
justifyContent: 'center',
marginTop: '20px',
marginBottom: '10px',
}}>
<Form.Control
as='textarea'
rows={5}
placeholder='enter something here'
onChange={this.props.handleInput}
style={{ width: '500px' }}
/>
</div>
<div
style={{
display: 'flex',
justifyContent: 'center',
}}>
<Button
variant='primary'
onClick={this.props.handleClick}>
Submit
</Button>
</div>
</div>
</Form.Group>
</Form>
</Container>
<div
style={{
display: 'flex',
justifyContent: 'center',
}}>
<h1 value={this.props.input}>
{this.props.output}
</h1>
</div>
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
action.js
import {
CHANGE_INPUT_FIELD,
POST_OUTPUT,
} from './constant';
export const changeInput = (text) => ({
type: CHANGE_INPUT_FIELD,
payload: text,
});
export const postOutput = (text) => ({
type: POST_OUTPUT,
payload: text,
});
reducer.js
import {
CHANGE_INPUT_FIELD,
POST_OUTPUT,
} from './constant';
const initialStateInput = {
input: '',
};
const initialStateOutput = {
output: '',
};
export const getInput = (
state = initialStateInput,
action = {}
) => {
switch (action.type) {
case CHANGE_INPUT_FIELD:
return Object.assign({}, state, {
input: action.payload,
});
default:
return state;
}
};
export const getOutput = (
state = initialStateOutput,
action = {}
) => {
switch (action.type) {
case POST_OUTPUT:
return Object.assign({}, state, {
output: action.payload,
});
default:
return state;
}
};
constant.js
export const CHANGE_INPUT_FIELD =
'CHANGE_INPUT_FIELD';
export const POST_OUTPUT = 'POST_OUTPUT';
changeInput action must be handled inside the component there is no reason to dispatch an action and handle it with reducer because reducer is for managing shared states.
Can you specify what is the "problem"?
The problem is not with actions, you cannot see the value because the value is set to undefined
In App.js you have to pass the correct value
onClick={this.props.handleClick}> must change as onClick={this.props.handleClick(this.props)}> otherwise props will be equal to event object in the line handleClick: (props) => dispatch(postOutput(props.output))
Still you won't see the value in UI because the output value is set to '' because you are not setting the input value to the output value in reducer.
My suggestion there must be another action that fires when submit button is clicked and sets the current input value to the input, then fire getOutput
I have created a function which fetches data from API and send a response of userDetail, which has been created in context. and function has been called in Dashboard component by importing context. And when user clicks button it render userData.
I am tried testing to check function has been called once or not, after click. but not able to achieve it.
Context Page
here userDataFunc is created .
import React, { Component, createContext } from "react";
import axios from "axios";
export const Contx = createContext();
export class ConProvider extends Component {
state = {
userData: []
};
userDataFunc = async () => {
await axios(`https://jsonplaceholder.typicode.com/users`)
.then((res) => {
if (res.status === 200) {
this.setState({
userData: res.data
});
}
})
.catch((err) =>
this.setState({
userDataerror: err
})
);
};
render() {
console.log(this.state.coin);
return (
<Contx.Provider
value={{
...this.state,
userDataFunc: this.userDataFunc
}}
>
{this.props.children}
</Contx.Provider>
);
}
}
Dashboard Component
Here Function and userdata state has been imported from context
import React, { useContext } from "react";
import { Contx } from "../ContextApi";
export default function Dashboard() {
const { userDataFunc, userData } = useContext(Contx);
return (
<div
style={{ height: "100vh", backgroundColor: "#151515" }}
className="d-flex justify-content-center align-items-center"
>
<button data-testid="renderData" onClick={userDataFunc}>
Render Data
</button>
<div
style={{
overflowY: "auto",
margin: "4px",
border: "2px solid",
padding: "12px",
height: "80vh"
}}
data-testid="tableData"
>
{userData.map((i) => {
return (
<h5 key={i.id} style={{ color: "#fff" }}>
{i.name}
</h5>
);
})}
</div>
</div>
);
}
Dashboard Test File
// import React from "react";
import { fireEvent, render } from "#testing-library/react";
import axiosMock from "axios";
import Dashboard from "./Dashboard";
import { Contx } from "../ContextApi";
jest.mock("axios");
it("Api Called", () => {
const { getByTestId } = render(
<Contx>
<Dashboard />
</Contx>
);
const renderButton = getByTestId("userData");
fireEvent.click(renderButton);
expect(Dashboard.userDataFunc()).toHaveBeenCalledTimes(1);
});
I recently started to use Next.JS on a new project and it's working fine but I can't figure out how to keep my layout state active on client side when I throw a 404 error after clicking on a wrong link.
Thanks to Adam's Wathan article, I'm able to share my state between different pages :
On home page
On "about" page
But if i click on "error", it will render the _error.tsx without preserving data i wrote in the input.
This page seems to render the whole app tree on server side, despite the fact I provide the same layout. Is there anyway to prefetch this page, just like a regular one and avoid to lose some information without using a solution like Redux ? I'm note quite familiar with it and it seems a bit too much in this case.
Here is my code:
pages/_error.tsx
import { getLayout } from "../components/layout/mainLayout"
import { withTranslation } from "../i18n";
import { FlexDirectionProperty } from "csstype";
const imgStyle = {
maxWidth: "100%",
maxHeight: "100%"
};
const figureStyle = {
height: "80vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column" as FlexDirectionProperty
};
const CustomError: any = ({ status, t }: any) => (
<>
<figure style={figureStyle}>
<figcaption>
<h1>Statut:{t('WELCOME')}</h1>
</figcaption>
<img
alt="Showing a properly cat according the status code"
src={`https://http.cat/${status}`}
style={imgStyle}
/>
</figure>
</>
)
CustomError.getLayout = getLayout;
CustomError.getInitialProps = async ({ res, err }: any) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : null
return {
statusCode: statusCode,
namespacesRequired: ["common"]
}
};
export default withTranslation('common')(CustomError)
components/layout/header.tsx
import Link from "next/link";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import { withTranslation, i18n } from "../../i18n";
import { capitalize } from "../../helpers/utils";
import Modal from "react-bootstrap/Modal";
import { useState } from "react";
const loadStamp = +new Date();
const Header: any = ({ t }: any) => {
const [show, setShow] = useState(false);
const [active, setActive] = useState("home");
return (
<>
<Navbar fixed="top" bg="light">
<Nav className="mr-auto" activeKey={active}>
<Nav.Item>
<Link href="/">
<Navbar.Brand onClick={() => setActive("home")} href="/">Random Project</Navbar.Brand>
</Link>
</Nav.Item>
...
</Nav>
</Navbar>
</>);
};
export default withTranslation("common")(Header);
components/layout/mainLayout.tsx
import Header from "./header";
import "bootstrap/dist/css/bootstrap.min.css";
import "../../public/stylesheets/style.scss";
type LayoutProps = {
title?: string;
children: any;
};
const Layout: React.FunctionComponent<LayoutProps> = ({ children }) => (
<>
<Header />
<main role="main" className="main">
{children}
</main>
</>
);
export const getLayout: any = (page: any) => <Layout>{page}</Layout>
export default Layout
And my _app.tsx
import React from 'react'
import App from 'next/app'
import { appWithTranslation } from '../i18n'
class MyApp extends App<any> {
render() {
const { Component, pageProps } = this.props
const getLayout = Component.getLayout || ((page: any) => page)
return (
<>
{getLayout(
<Component {...pageProps}></Component>
)}
</>
)
}
}
export default appWithTranslation(MyApp)
I need to implement a <BackButton /> in react-admin for example when I open show page for a resource I need able to back to the list page.
Can you guide me to implement this?
I'm not familiar with react-admin routing mechanism.
Now I'm using this component in my edit form actions props:
const MyActions = ({ basePath, data, resource }) => (
<CardActions>
<ShowButton basePath={basePath} record={data} />
<CloneButton basePath={basePath} record={data} />
{/* Need <BackButton /> here */}
</CardActions>
);
export const BookEdit = (props) => (
<Edit actions={<MyActions />} {...props}>
<SimpleForm>
...
</SimpleForm>
</Edit>
);
You can use react-router-redux's goBack() function to achieve this.
For example, your button component will look something like this:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Button from '#material-ui/core/Button';
import { goBack } from 'react-router-redux';
class BackButton extends Component {
handleClick = () => {
this.props.goBack();
};
render() {
return <Button variant="contained" color="primary" onClick={this.handleClick}>Go Back</Button>;
}
}
export default connect(null, {
goBack,
})(BackButton);
Now use that button component in your CardActions.
You can get help from an example which uses react-router-redux's push() function in a similar way from the official docs.
Create a back button. This one will pass props and children (text) and uses react-router directly, which I think makes more sense and keeps your code simple.
// BackButton.js
import React from 'react'
import Button from '#material-ui/core/Button'
import { withRouter } from 'react-router'
const BackButton = ({ history: { goBack }, children, ...props }) => (
<Button {...props} onClick={goBack}>
{children}
</Button>
)
export default withRouter(BackButton)
Example usage:
import { Toolbar, SaveButton } from 'react-admin'
import BackButton from './BackButton'
const SomeToolbar = props => (
<Toolbar {...props}>
<SaveButton />
<BackButton
variant='outlined'
color='secondary'
style={{ marginLeft: '1rem' }}
>
Cancel
</BackButton>
</Toolbar>
)
The complete code is below.
//BackButton.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import compose from 'recompose/compose';
import { withStyles, createStyles } from '#material-ui/core/styles';
import { translate } from 'ra-core';
import Button from '#material-ui/core/Button';
import ArrowBack from '#material-ui/icons/ArrowBack';
import classnames from 'classnames';
import { fade } from '#material-ui/core/styles/colorManipulator';
const styles = theme =>
createStyles({
deleteButton: {
color: theme.palette.error.main,
'&:hover': {
backgroundColor: fade(theme.palette.error.main, 0.12),
// Reset on mouse devices
'#media (hover: none)': {
backgroundColor: 'transparent',
},
},
},
});
const sanitizeRestProps = ({
basePath,
className,
classes,
label,
invalid,
variant,
translate,
handleSubmit,
handleSubmitWithRedirect,
submitOnEnter,
record,
redirect,
resource,
locale,
...rest
}) => rest;
class BackButton extends Component {
static contextTypes = {
router: () => true, // replace with PropTypes.object if you use them
}
static propTypes = {
label: PropTypes.string,
refreshView: PropTypes.func.isRequired,
icon: PropTypes.element,
};
static defaultProps = {
label: 'ra.action.back',
icon: <ArrowBack />,
};
render() {
const {
className,
classes = {},
invalid,
label = 'ra.action.back',
pristine,
redirect,
saving,
submitOnEnter,
translate,
icon,
onClick,
...rest
} = this.props;
return (
<Button
onClick={this.context.router.history.goBack}
label={label}
className={classnames(
'ra-back-button',
classes.backButton,
className
)}
key="button"
{...sanitizeRestProps(rest)}>
{icon} {label && translate(label, { _: label })}
</Button>
)
}
}
const enhance = compose(
withStyles(styles),
translate
);
export default enhance(BackButton);
//Toolbar.js
import React from 'react';
import {
Toolbar,
SaveButton,
DeleteButton,
} from 'react-admin';
import { withStyles } from '#material-ui/core';
import BackButton from './BackButton'
const toolbarStyles = {
toolbar: {
display: 'flex',
justifyContent: 'space-between',
},
};
export const CustomEditToolbar = withStyles(toolbarStyles)(props => (
<Toolbar {...props}>
<SaveButton/>
<DeleteButton/>
<BackButton/>
</Toolbar>
));
export const CustomCreateToolbar = withStyles(toolbarStyles)(props => (
<Toolbar {...props}>
<SaveButton/>
<BackButton/>
</Toolbar>
));