React Context API and child component refresh - javascript

I have a Context component which, when I set a certain value, I'd like them to effect other components using it, but my understanding seems wrong here.
I have a MainComponent component that basically handles the layout of the screen.
import { Consumer } from './context';
class MainContainer extends React.Component {
render() {
const noPadding = {
margin: 0,
padding: 0
}
const isAuthenticated = this.context.isAuthenticated()
return (
<HashRouter>
<Header />
<Container>
{isAuthenticated &&
<AuthorisedArea>
<Row style={noPadding}>
<Col md="3" style={noPadding}>
<RightInfoBar />
</Col>
<Col md="9">
<RouteManager />
</Col>
</Row>
</AuthorisedArea>
}
</Container>
</HashRouter>
)
}
}
MainContainer.contextType = Consumer;
export default MainContainer
This should all work when the user is logged in. When they're logged out, for now, I expect a blank screen, really.
In my Navbar, when a user clicks logout, I do this:
handleLogout() {
const { toastManager } = this.props;
const { pathname } = this.props.location;
this.context.changeAuthenticated(false);
this.context.logout();
if(pathname !== "/") {
this.props.history.push("/");
}
toastManager.add('You\'re now logged out', {
appearance: 'success',
autoDismiss: true,
pauseOnHover: false,
});
}
the this.context.logout updates a property in my context component, setting 'isAuthenticated' to false.
So my basic context component:
const Context = React.createContext();
export class Provider extends React.Component {
state = {
user: {
name: "",
email: "",
authStatus : Auth.isAuthenticated()
},
changeEmail: (newEmail) => {
let user = this.state.user;
user.email = newEmail;
this.setState({ user: user})
},
changeAuthenticated: (newState) => {
if(newState ===false) Auth.logout();
let user = this.state.user;
user.name = "Craig";
this.setState ({ user: user });
},
logout: () => {
console.log("Will logout");
Auth.logout();
this.setState({authStatus: false});
},
isAuthenticated: () => {
return Auth.isAuthenticated()
}
}
render() {
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
}
export const Consumer = Context.Consumer;
So, my main component, I was hoping, would notice that isAuthenticated goes to false... and the screen would go blank (for now... I will do better things once I have this working).
But when I click logout - the console indicates the method in the Context component fired... and the value becomes false... But the screen remains.
Only when I press F5, does the screen go as expected.
How should I be making my MainComponent react to React.Context value changes?

Related

Mock context Consumer (React) for testing component

I'm trying to write a test to check if the component Header is rendering the company logo (context.company?.logoUrl, via context) and if it receives the companyId, via props.
import { Container, Image } from "react-bootstrap";
type Props = {
context: AppContextProps;
companyId: string | undefined;
};
const Header = ({ context, companyId }: Props) => {
if (!context.company) {
return <label>Loading...</label>;
}
return (
<Container className="header">
<a href={`/${companyId}`}>
<Image
src={context.company?.logoUrl}
className="header-logo"
/>
</a>
</Container>
);
};
export default WithContext(Header);
The component Header is wrapped by a high order component, WithContext.
const WithContext = Component => {
return props => (
<AppContext.Consumer>
{({state}) => <Component context={ state } {...props} />}
</AppContext.Consumer>
);
}
export default WithContext;
And here it is the AppContext structure.
export const AppContext = React.createContext();
class AppContextProvider extends Component {
state = {
company: null,
};
getCompanyData() {
try {
const response = await companyService.getPublicProfile();
this.setState({ company: response });
} catch (error) {
console.log({ error });
}
}
componentDidMount() {
this.setState({
company: this.getCompanyData,
});
}
render() {
return (
<AppContext.Provider value={{ state: this.state }}>
{this.props.children}
</AppContext.Provider>
);
}
}
export default AppContextProvider;
This code was written 4 years ago, that's the reason why we are using class components and context this way (I know it is not the best, but we need to keep it that way). 😊
Here it is the base of the test (Jest one).
it("should load company logo URL", async () => {
const propsMock = {
companyId: "abcd9876",
};
const contextMock = {
company: {
logoUrl: "https://picsum.photos/200/300",
},
};
render(
<AppContext.Consumer>
{({ state }) => <Header context={state} {...propsMock} />}
</AppContext.Consumer>
);
// Don't mind this expect :P
expect(1 + 2).toBe(3);
});
Every time I run the test, I get an error in Context.Consumer.
TypeError: Cannot destructure property 'state' of 'undefined' as it is
undefined.
render(
28 | <AppContext.Consumer>
> 29 | {({ state }) => <Header context={state} {...propsMock} />}
| ^
30 | </AppContext.Consumer>
31 | );
Basically I'm struggling to mock a Consumer and pass the context (state, which is supposed to be contextMock) and props (propsMock) so I can pass them to the Header component.
Do you guys have any idea how to make this test work properly?
Thank you!

How to hide some component based on some flag in react js

I want to hide some component based on some flag in react js.
I have an App component where I have Login and other components, I want to hide the other component until Login components this.state.success is false and on click of a button I am changing the sate, but it's not working, I am new to react,
My App Class compoenent -
import React, { Component } from "react";
import logo from "../../logo.svg";
// import Game from "../Game/Game";
import Table from "../Table/Table";
import Form from "../Table/Form";
import Clock from "../Clock/Clock";
import "./App.css";
import Login from "../Login/Login";
class App extends Component {
state = {
success: false
};
removeCharacter = index => {
const { characters } = this.state;
this.setState({
characters: characters.filter((character, i) => {
return i !== index;
})
});
};
handleSubmit = character => {
this.setState({ characters: [...this.state.characters, character] });
};
handleSuccess() {
this.setState({ success: true });
}
render() {
const { characters, success } = this.state;
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<span className="Span-inline">App</span>
<Clock time={new Date()} />
</header>
<Login success={success} handleSuccess={this.handleSuccess} />
{success && (
<div className="container">
<h1>React Tutorial</h1>
<p>Add a character with a name and a job to the table.</p>
<Table
characterData={characters}
removeCharacter={this.removeCharacter}
/>
<h3>Add New character</h3>
<Form handleSubmit={this.handleSubmit} />
</div>
)}
{/* <Game /> */}
</div>
);
}
}
export default App;
My Login component -
import React, { Component } from "react";
import Greeting from "./Greeting";
import LogoutButton from "./LogoutButton";
import LoginButton from "./LoginButton";
class Login extends Component {
constructor(props) {
super(props);
this.handleLoginClick = this.handleLoginClick.bind(this);
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = {
isLoggedIn: false,
name: "",
success: false
};
}
handleLoginClick() {
this.setState({ isLoggedIn: true });
this.setState({ success: true });
}
handleLogoutClick() {
this.setState({ isLoggedIn: false });
this.setState({ success: false });
}
onChange = e => {
this.setState({
name: e.target.value
});
};
render() {
const isLoggedIn = this.state.isLoggedIn;
const name = this.state.name;
// const successLogin = this.state.success;
let button;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}
return (
<div>
<Greeting
isLoggedIn={isLoggedIn}
name={name}
onChange={this.onChange}
/>
{button}
</div>
);
}
}
export default Login;
please guide me on what I am doing wrong.
Why sometime debuggers do not trigger in react component?
For the sake of example I have used functional stateless component here. You can use Class component all upto you.
const conditionalComponent = (props) => {
let condition = true;
return (
{condition && <div><h1>Hello world</h1></div>}
}
Instead of directly giving condition you can even call function which returns a boolean value.
handleLoginClick() {
this.setState({ isLoggedIn: true });
this.setState({ success: true });
this.props.handleSuccess()
}
do like this
<Login success={success} handleSuccess=
{this.handleSuccess} />
bind this function

ReactJS | Loading State in component doesn't render Spinner

I am trying to make a React component that displays multiple renders based on props and state. So, while I wait for the promise to be resolved, I want to display a spinner Component
Main Renders:
NoResource Component => When the user is not valid
Spinner Component => When is loading on all renders
BasicRender Component => When data are fetched and is not loading
Below is my component:
/* eslint-disable react/prefer-stateless-function */
import React, { Component, Fragment } from 'react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { getUser, listUsers } from '../../config/service';
export class UserDetailsScreen extends Component {
static propTypes = {
match: PropTypes.shape({
isExact: PropTypes.bool,
params: PropTypes.object,
path: PropTypes.string,
url: PropTypes.string
}),
// eslint-disable-next-line react/forbid-prop-types
history: PropTypes.object,
label: PropTypes.string,
actualValue: PropTypes.string,
callBack: PropTypes.func
};
state = {
user: {},
error: '',
isloading: false
};
componentDidMount() {
this.fetchUser();
this.setState({ isLoading: true})
}
getUserUsername = () => {
const { match } = this.props;
const { params } = match;
return params.username;
};
fetchUser = () => {
getUser(this.getUserUsername())
.then(username => {
this.setState({
user: username.data,
isloading: false
});
})
.catch(({ message = 'Could not retrieve data from server.' }) => {
this.setState({
user: null,
error: message,
isLoading: false
});
});
};
validateUsername = () =>
listUsers().then(({ data }) => {
const { match } = this.props;
if (data.includes(match.params.username)) {
return true;
}
return false;
});
// eslint-disable-next-line no-restricted-globals
redirectToUsers = async () => {
const { history } = this.props;
await history.push('/management/users');
};
renderUserDetails() {
const { user, error } = this.state;
const { callBack, actualValue, label, match } = this.props;
return (
<div className="lenses-container-fluid container-fluid">
<div className="row">
.. More Content ..
{user && <HeaderMenuButton data-test="header-menu-button" />}
</div>
{user && this.validateUsername() ? (
<Fragment>
.. Content ..
</Fragment>
) : (
<div className="container-fluid">
{this.renderNoResourceComponent()}
</div>
)}
<ToolTip id="loggedIn" place="right">
{user.loggedIn ? <span>Online</span> : <span>Oflline</span>}
</ToolTip>
</div>
);
}
renderNoResourceComponent = () => {
const { match } = this.props;
return (
<div className="center-block">
<NoResource
icon="exclamation-triangle"
title="Ooops.."
primaryBtn="« Back to Users"
primaryCallback={this.redirectToUsers}
>
<h5>404: USER NOT FOUND</h5>
<p>
Sorry, but the User with username:
<strong>{match.params.username}</strong> does not exists
</p>
</NoResource>
</div>
);
};
renderSpinner = () => {
const { isLoading, error } = this.state;
if (isLoading && error === null) {
return <ContentSpinner />;
}
return null;
};
render() {
return (
<div className="container-fluid mt-2">
{this.renderSpinner()}
{this.renderUserDetails()}
</div>
);
}
}
export default withRouter(UserDetailsScreen);
The problem is:
I get the spinner along with the main component, and I am getting this error:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.. Can you please tell me what I am doing wrong.
The error is because you are running the renderUserDetailsComponent even when your API call is in loading state. You must only render the spinner on loading state
renderUserDetails() {
const { user, error, isLoading } = this.state;
if(isLoading) {
return null;
}
const { callBack, actualValue, label, match } = this.props;
return (
<div className="lenses-container-fluid container-fluid">
<div className="row">
.. More Content ..
{user && <HeaderMenuButton data-test="header-menu-button" />}
</div>
{user && this.validateUsername() ? (
<Fragment>
.. Content ..
</Fragment>
) : (
<div className="container-fluid">
{this.renderNoResourceComponent()}
</div>
)}
<ToolTip id="loggedIn" place="right">
{user.loggedIn ? <span>Online</span> : <span>Oflline</span>}
</ToolTip>
</div>
);
}

How to combine Material-UI's snackbar and input components in react?

I'm using Material-UI components to build my website. I have a header component with a search field which uses mui InputBase under the hood. When user enters empty input (that is they don't enter anything and just click enter) I want to display a mui Snackbar which will warn the user the no meaningful input was entered.
I can't get the combination of the two components to work together. In addition because search field state doesn't really change when user enters nothing it doesn't reload so if user repeatedly presses Enter the snackbar won't appear. I use this.forceUpdate(); but is there a more elegant way to implement such logic?
This is my code:
for the search input field:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import InputBase from '#material-ui/core/InputBase';
import { withStyles } from '#material-ui/core/styles';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { getAppInfo } from '../../actions/appActions.js';
import constants from '../../constants.js';
import { AppSearchBarInputStyles } from '../styles/Material-UI/muiStyles.js';
import AppNotFound from './AppNotFound.js';
class AppSearchBarInput extends Component {
state = {
appId: ''
}
onChange = e => {
this.setState({ appId: e.target.value });
}
onKeyDown = e => {
const { appId } = this.state;
if (e.keyCode === constants.ENTER_KEY && appId !== '') {
this.props.getAppInfo({ appId });
this.setState({
appId: ''
});
}
this.props.history.push('/app/appInfo');
this.forceUpdate();
}
render() {
const { classes } = this.props;
const { appId } = this.state;
console.log(`appId from AppSearchInput=${appId === ''}`);
return (
<div>
<InputBase
placeholder="Search…"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
value={this.state.appId} />
{ appId === '' ? <AppNotFound message={constants.MESSAGES.APP_BLANK()}/> : ''}
</div>
)
}
}
AppSearchBarInput.propTypes = {
classes: PropTypes.object.isRequired
}
const AppSearchBarWithStyles = withStyles(AppSearchBarInputStyles)(AppSearchBarInput);
const AppSearchBarWithStylesWithRouter = withRouter(AppSearchBarWithStyles);
export default connect(null, { getAppInfo })(AppSearchBarWithStylesWithRouter);
for the snackbar:
import React from 'react';
import Snackbar from '#material-ui/core/Snackbar';
import constants from '../../constants.js';
import SnackbarMessage from './SnackbarMessage.js';
class AppNotFound extends React.Component {
state = {
open: true,
};
handleClose = event => {
this.setState({ open: false });
};
render() {
const { message } = this.props;
return (
<Snackbar
anchorOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={this.state.open}
autoHideDuration={6000}
onClose={this.handleClose}
>
<SnackbarMessage
onClose={this.handleClose}
variant="warning"
message={message}
/>
</Snackbar>
);
}
}
export default AppNotFound;
I think the good way to achieve what You want is by adding another state property called snackBarOpen which will help You to determine if user has entered empty value or something meaningful:
AppSearchBarInput Component
state = {
appId: '',
snackBarOpen: false
}
handleKeyDown = (e) => {
if (e.keyCode === 13 && e.target.value === '') {
this.setState({
appId: e.target.value,
snackBarOpen: true
});
} else {
this.setState({
appId: e.target.value
})
}
}
handleCloseSnackBar = () => {
this.setState({
snackBarOpen: false
});
}
And then just render also <AppNotFound /> in render() method(it will be hidden by default because it will depend on open prop):
render() {
const { snackBarOpen } = this.state;
return(
<div>
/* <InputBase /> here */
<AppNotFound message={/* Your message here */} open={snackBarOpen} onClose={this.handleCloseSnackBar} />
</div>
)
}
AppNotFound Component
You can remove all methods now and leave only render() one which will look next:
render() {
const { message, open, onClose } = this.props;
return (
<Snackbar
// ...
open={open}
// ...
onClose={onClose}
>
<SnackbarMessage
onClose={onClose}
// ...
message={message}
/>
</Snackbar>
);
}
Hope that my answer will be useful :)

show another component as soon as the user logs in

I have a use case where the user gets to log in. As soon as the user gets log in, another component should be shown. This is not working to me. I have to hit the login button again to show another component or has to refresh the page.
Here is what I have done
This is the parent component
const mapStateToProps = state => {
return {
user: state.loginReducer
}
}
class App extends React.Component {
state = {
hasToken: false
}
componentDidMount() {
const { user } = this.props;
window.chrome.storage.sync.get(['user_token'], result => {
if ((user && user.access_token) || result.user_token) {
console.log('user_token in cdm', result, user);
this.setState({ hasToken: true })
}
})
}
componentWillReceiveProps(nextProps) {
if (this.props.user !== nextProps.user) {
window.chrome.storage.sync.get(['user_token'], result => {
if (nextProps.user.length || result.user_token) {
this.setState({ hasToken: true })
}
})
}
}
anotherComponent() { // just to show the demo
return (
<div class="content">
component to show when the user logs in
</div>
)
}
render() {
const { hasToken } = this.state;
return (
<div>
<Header />
{ !hasToken ? <Login /> : this.anotherComponent()}
</div>
)
}
}
export default connect(mapStateToProps, null)(App);
login.js
const mapDispatchToProps = dispatch => ({
userLogin: user => dispatch(login(user))
})
class Login extends React.Component {
state = {
user: {
email:"",
password: "",
grant_type: "password"
}
}
handleChange = e => {
this.setState({user: {...this.state.user, [e.target.name]: e.target.value}})
}
handleSubmit = e => {
e.preventDefault();
this.props.userLogin(this.state.user);
}
render() {
const { user } = this.state;
return (
<Grid>
<Row className="pad-10">
<Col sm={12} md={6} mdOffset={3}>
<Form onSubmit={this.handleSubmit}>
<FormGroup controlId="email">
<ControlLabel>Email</ControlLabel>
<FormControl
type="email"
placeholder="Email"
name="email"
onChange={this.handleChange}
value={user.email}
/>
</FormGroup>
<FormGroup controlId="password">
<ControlLabel>Password</ControlLabel>
<FormControl
type="password"
placeholder="Password"
name="password"
onChange={this.handleChange}
value={user.password}
/>
</FormGroup>
<FormGroup>
<Button type="submit">Sign in</Button>
</FormGroup>
</Form>
</Col>
</Row>
</Grid>
);
}
}
export default connect(null, mapDispatchToProps)(Login);
I am not using any router concept so What I wanted to do is when user hits the login button, if the login is successful, the token is respond from the server and that is checked so that if it is successful, the user will be shown another component.
UPDATE
export const login = action(LOGIN, 'user');
export const loginSuccess = action(LOGIN_SUCCESS, 'data');
export const loginFailure = action(LOGIN_FAILURE, 'error');
reducer code
const initialState = {
fetching: false,
error: null,
user: []
}
function loginReducer(state=initialState, action) {
switch (action.type) {
case LOGIN:
return {...state, fetching: true}
case LOGIN_SUCCESS:
return {...state, fetching: false, user: action.data.access_token}
case LOGIN_FAILURE:
return {...state, fetching: false, error: action.error}
default:
return state;
}
}
export default loginReducer;
I don't know exactly how does window.chrome.storage.sync works, but obvious solution (at the first glance) is:
// parent component
render() {
const { user } = this.props;
return (
<div>
<Header />
{ !user ? <Login /> : this.anotherComponent()}
</div>
)
}
You have to get user from your state
I'll provide more details when you bring your reducers/actions
The problem your code is not working is because the App component is only rendered once irrespective of weather the user is logged in or not. So later when the user is logged in, your app component is not re-rendered.
There could be many ways to solve this. What I suggest you to do is something like this:
pass an onUserLogin callback to Component something like
<LoginModal isModalOpen={isModalOpen} onLoginClick={this.onLoginClick} />
Then do a setState inside the onLoginClick function to make sure component is rendered with changed props.
onLoginClick = () => {
if (!this.state.isUserLoggedIn) {
this.setState({
isModalOpen:!this.state.isModalOpen,
});
}
}

Categories