Tips for displaying components based on global state - javascript

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.

Related

how can i make a component re render in react

so i'm creating my first fullstack website and once a user signs in it gets stored in the localStorage and i want to display the name of the user in my header once he is logged in but my header is not re rendering so nothing happens : this is the header before logging in
header
and this is how i want it to Be after signing in :
header after logging in this is my Layout code:
import "../assets/sass/categoriesbar.scss";
import Header from "./Header/Header";
const Layout = (props) => {
return (
<>
<Header/>
<main>
{ props.children}
</main>
</>
);
}
export default Layout;
and this is the toolBar in my Header :
const ToolBar = () => {
const history = useHistory();
let currentUser= JSON.parse(localStorage.getItem("user-info"));
const logoutHandler = () => {
localStorage.clear("user-info");
history.push("/login");
};
return (
<>
<div className={classes.NavigationBar}>
<h1>
<Link to="/">Pharmashop</Link>
</h1>
<NavLinks logout={logoutHandler}/>
{localStorage.getItem("user-info")?
<h5>Welcome {currentUser.name} !</h5>
:
<RegisterButton />
}
</div>
</>
);
};
export default ToolBar;
please help me it's frustrating
PS: this is my first stackoverflow question sorry if it's unorganized and unclear and sorry for my bad english.
Hazem, welcome to Stack Overflow.
In react, if you want the component to re-render when some data changes, that info must be in the component state. In your code the current user is a const, not bind to the component's state. This is how it could auto re-render when the user logs in:
const ToolBar = () => {
const [currentUser, setCurrentUser] = useState(JSON.parse(localStorage.getItem("user-info")));
const logoutHandler = () => {
localStorage.clear("user-info");
history.push("/login");
};
return (
<>
<div className={classes.NavigationBar}>
<h1>
<Link to="/">Pharmashop</Link>
</h1>
<NavLinks logout={logoutHandler}/>
{currentUser?
<h5>Welcome {currentUser.name} !</h5>
:
<RegisterButton />
}
</div>
</>
);
};
export default ToolBar;
See more about state in the official documentation.

How do I integrate my Switch component into my Table component as a separate column, and have separate state on both?

I am new to React.js, and so far, I am loving it. I am still confused on the concept of stateful components, although. I am using Bootstrap tables to build my table, and my GET request for its data grab worked flawlessly. I am using the material-ui lib for my switch component as well (no need to reinvent the wheel here!)
Although, I am now trying to integrate a new column that will be a switch for each row in my table, and that, when toggled, changes the boolean of said switch to true/false, which will then send a PUT request down to my backend. I have not built my PUT request yet, as I cannot get this UI portion functioning. Here is my code so far, and the dumby UI works, but I don't know how to integrate the stateful render I defined in NodeTableContainer at <SwitchState/> and SwitchState(), into my definition at selectionRenderer: Switches in my NodeTable component. The stateful render does render a toggle switch under the table, essentially as its own independent component. But I want to integrate that toggle switch component in const selectRow = {mode: 'checkbox', clickToSelect: true,selectionRenderer: Switches}. Here is my code, and I hope my I have explained my issue well. I have Googled endlessly, but I believe my own ignorance has blocked my from discovering the answer I need.
Table Component (NodeTable)
import React from 'react';
import {
Row,
Col,
Card,
CardBody,
} from 'reactstrap';
import BootstrapTable from 'react-bootstrap-table-next';
import ToolkitProvider, { Search, CSVExport, ColumnToggle } from 'react-bootstrap-table2-toolkit';
import paginationFactory from 'react-bootstrap-table2-paginator';
import 'chartjs-plugin-colorschemes';
import Switches from './Switch'
const columns = OMIT
const defaultSorted = [
{
dataField: 'id',
order: 'asc',
},
]
const TableWithSearch = (props) => {
const { SearchBar } = Search;
const { ExportCSVButton } = CSVExport;
const selectRow = {
mode: 'checkbox',
clickToSelect: true,
selectionRenderer: Switches
}
return (
<Card>
<CardBody>
<h4 className="header-title">OMIT</h4>
<p className="text-muted font-14 mb-4">OMIT</p>
<ToolkitProvider
bootstrap4
keyField="fqn"
data={props.data}
columns={columns}
columnToggle
search
exportCSV={{ onlyExportFiltered: true, exportAll: false }}>
{props => (
<React.Fragment>
<Row>
<Col>
<SearchBar {...props.searchProps} />
</Col>
<Col className="text-right">
<ExportCSVButton {...props.csvProps} className="btn btn-primary">
Export CSV
</ExportCSVButton>
</Col>
</Row>
<BootstrapTable
{...props.baseProps}
bordered={false}
defaultSorted={defaultSorted}
pagination={paginationFactory({ sizePerPage: 5 })}
selectRow={selectRow}
wrapperClasses="table-responsive"
/>
</React.Fragment>
)}
</ToolkitProvider>
</CardBody>
</Card>
);
};
export default TableWithSearch;
Switch Component
// #flow
import React from 'react';
import 'chartjs-plugin-colorschemes';
import './Switch.css'
import Switch from '#material-ui/core/Switch';
export default function Switches({ isOn, handleToggle }) {
return (
<div>
<Switch
checked={isOn}
onChange={handleToggle}
name="checkedA"
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>
</div>
);
}
Parent Component (NodeTableContainer)
import axios from 'axios';
import React, { Component, useState } from 'react';
import Switch from './Switch';
import App from './index';
export default class MainComp extends React.Component {
state = {
nodesData: [],
chartRef: [],
conn: [],
switchstate: [],
}
componentDidMount() {
axios.get('OMIT')
.then(res => {
const nodestate = res.data.map(x => x.nodestate);
for (var i = 0; i < nodestate.length; i++) {
if (nodestate[i] == 'up') {
nodestate[i] = true;
}
else {
nodestate[i] = false;
}
}
this.setState({ nodesData: res.data, switchstate: nodestate });
})
}
render() {
return (
< >
<App data={this.state.nodesData} checked={this.state.switchstate} />,
<SwitchState />
</>
)
}
}
function SwitchState() {
const [value, setValue] = useState(false);
console.log(value)
return (
<div className="app">
<Switch
isOn={value}
onColor="#EF476F"
handleToggle={() => setValue(!value)}
/>
</div>
);
}
Also, my SwitchState component is in a dumby form as you will see, until I can see the log showing its boolean state changing. Also, nodestate in the NodeTableContainer was my pathetic try at pulling data via the same state data. That is nonfunctional as you will also see. I will build the state properly once I can get this figured out, or you wonderful individuals aid me in this as well. Again, I am showing my ignorance here, so if there is an easier way, or if I am using an odd flavor of libs for this, please let me know. I want to learn and thrive. If you have a solution of your own, that's a completely different flavor, I plea to you to share it! Thank you all!
I figured this out for react-bootstrap. I fat arrowed in the formatter, and passed the state to formatExtraData. I then pass state from my component that holds all state, and it works flawlessly. Time to integrate my PUT request in with the event handler!
Below are my changes in code:
Table Component
export default class TableComp extends React.Component
formatter: (cell, row, index, extra) => {
if (cell === 'up') {
cell = true
}
else {
cell = false
}
return (
<div>
<Switch
checked={cell}
onChange={extra.handle}
name={row.name}
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>
</div>
)
},
formatExtraData: { handle: this.props.handleClick }
Parent Component (Holds all state)
handleClick = (e) => {
var indexFound = this.state.data.findIndex(y => y.name === e.target.name.toString())
let data= [...this.state.data];
let item = { ...data[indexFound] }
if (item.state === 'up') {
item.state = 'down'
}
else {
item.state = 'up'
}
data[indexFound] = item
this.setState({ data})
}

Component only rending if I start the flow from the homepage

I am having an issue with my application. My user component only loads UserCard when I start the application from the homepage then click users link there... if I just refresh the users URL... UserCard doesn't get loaded which means something is wrong with my this.props.users. I do see that in chrome it says: Value below was evaluated just now when I refresh but when I go through the flow it doesn't say that. Any help will be appreciated.
App.js
class App extends Component {
constructor(props) {
super(props);
this.state = {
users: []
};
}
componentDidMount() {
users = []
axios.get('/getall').then((res) => {
for(var d in res.data) {
users.push(new User(res.data[d]));
}
});
this.setState({ users });
}
render() {
const { users } = this.state;
return (
<Router history={history}>
<Switch>
<PrivateRoute exact path="/" component={Home} />
<Route exact path='/users' render={(props) => <Users {...props} users={users} />}/>
</Switch>
</Router>
)
}
}
PrivateRoute:
export const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
<Component {...props} /> )} />
)
User.js
export default class Users extends Component {
render() {
console.log(this.props.users);
return (
<Row>
{this.props.users.map(u =>
<UserCard key={u.name} user={u}/>
)}
</Row>
);
}
}
export class User {
constructor(obj) {
for (var prop in obj){
this[prop] = obj[prop];
}
}
getURLName() {
return this.name.replace(/\s+/g, '-').toLowerCase();
}
}
class UserCard extends Component {
render() {
return (
<Link to={'/users/' + this.props.user.getURLName()} >
<div>
// Stuff Here
</div>
</Link>
);
}
}
As per the comments:
The issue here is how you're setting state. You should never modify state directly since this will not cause the component to rerender See the react docs
Some additional thoughts unrelated to the question:
As per the comments - use function components whenever possible, especially with hooks on the way
There is probably no need to create a User class, only to new up little user objects. Simply use plain old JS objects and calculate the link url right in the place its used:
render() {
const { user } = this.props
return <Link to={`/users/${user.name.replace(/\s+/g, '-').toLowerCase()}`} />
}
It might be a good idea to start using a linter such as eslint. I see that you're declaring users = [] without using let or const (don't use var). This is bad practice since creating variables in this way pollutes the global name space. Linters like eslint will help you catch issues like this while you're coding.

React with react-router-dom 4 -- Cannot read property 'params' of undefined

I've been working on learning React to see if it suits my organization's needs, so needless to say I'm new at it. I've got a sample app that I've been working on to see how it works. I've gone through several of the answers here and haven't found one that fixes my problem.
I'm running into the problem where I get a "Uncaught (in promise) TypeError: Cannot read property 'params' of undefined" in the "componentDidMount()" at "const { match: { params } } = this.props;" method in the component below. I have a very similar component that takes an id from the url, using the same method, and it works fine. I'm confused as to why one is working and another isn't. I'm probably just making a rookie mistake somewhere (perhaps more than one), any hints/answers are appreciated.
The routing:
class App extends Component {
render() {
return (
<BrowserRouter>
<div>
<Route path='/' component={BaseView} />
<Route path='/test' component={NameForm} />
<Route path='/home' component={Home} />
<Route path='/quizzes' component={ViewQuizzes} />
<Route path='/comment/:rank' component={GetCommentsId} /*The one that works*//>
<Route path='/comment/edit/:testid' component={GetCommentEdit} /*The one I'm having trouble with*//>
<Route path='/comments' component={GetCommentsAll} />
</div>
</BrowserRouter>
);
}
}
The working component:
class GetCommentsId extends Component{
constructor (props) {
super(props)
this.state = {
Comments: [],
output: "",
wasClicked: false,
currentComment: " ",
}
this.handleCommentChange = this.handleCommentChange.bind(this);
}
componentDidMount(){
const { match: { params } } = this.props;
const url = 'http://localhost:51295/api/Values/' + params.rank;
axios.get(url).then(res => {
const comments = res.data;
this.setState({ comments });
this.output = (
<div>
<ul>
{ this.state.comments.map
(
comment =>
(<Comment
QuizId = {comment.Rank}
FirstName = {comment.FirstName}
Comments = {comment.Comments}
TestId = {comment.TestimonialId}
/>)
)}
</ul>
</div>
);
//console.log("From did mount: " + this.currentComment);
this.forceUpdate();
});
}
componentDidUpdate(){}
handleCommentChange(event){
//console.log("handle Comment Change activated");
}
handleClick(comment){
this.wasClicked = true;
this.currentComment = comment.Comments;
console.log(comment.Comments);
this.forceUpdate();
}
render () {
if(this.output != null){
if(!this.wasClicked){
return (this.output);
}
else{
console.log("this is the current comment: " + this.currentComment);
return(
<div>
{this.output}
<NameForm value={this.currentComment}/>
</div>
);
}
}
return ("loading");
}
}
The one that isn't working:
class GetCommentEdit extends Component {
constructor (props) {
super(props)
this.state = {
Comments: [],
output: "",
match: props.match
}
}
componentDidMount(){
const { match: { params } } = this.props;
const url = 'http://localhost:51295/api/Values/' + params.testid;
axios.get(url).then(res => {
const comments = res.data;
this.setState({ comments });
this.output = (
<div>
<ul>
{ this.state.comments.map
(comment =>
(<EditComment
QuizId = {comment.Rank}
FirstName = {comment.FirstName}
Comments = {comment.Comments}
TestId = {comment.TestimonialId}
/>)
)}
</ul>
</div>
);
//console.log("From did mount: " + this.currentComment);
this.forceUpdate();
});
}
render(){
return(
<div>
{this.output}
</div>
);
}
}
I've created a small app for you to demonstrate how to implement working react router v4.
On each route there is a dump of props, as you can see the params are visible there.
In your code I don't see why you are not using Switch from react-router v4, also your routes don't have exact flag/prop. This way you will not render your component views one after another.
Link to sandbox: https://codesandbox.io/s/5y9310y0zn
Please note that it is recommended to wrap withRouter around App component, App component should not contain <BrowserRouter>.
Reviewing your code
Please note that updating state triggers new render of your component.
Instead of using this.forceUpdate() which is not needed here, update your state with values you get from resolving the Promise/axios request.
// Bad implementation of componentDidMount
// Just remove it
this.output = (
<div>
<ul>
{this.state.comments.map
(
comment =>
(<Comment
QuizId={comment.Rank}
FirstName={comment.FirstName}
Comments={comment.Comments}
TestId={comment.TestimonialId}
/>)
)}
</ul>
</div>
);
//console.log("From did mount: " + this.currentComment);
this.forceUpdate();
Move loop function inside render method or any other helper method, here is code for using helper method.
renderComments() {
const { comments } = this.state;
// Just check if we have any comments
// Return message or just return null
if (!comments.length) return <div>No comments</div>;
// Move li inside loop
return (
<ul>
{comments.map(comment => (
<li key={comment.id}>
<Comment yourProps={'yourProps'} />
</li>
))}
</ul>
)
};
Add something like isLoading in your initial state. Toggle isLoading state each time you are done with fetching or you begin to fetch.
this.setState({ isLoading: true }); // or false
// Initial state or implement in constructor
state = { isLoading: true };
Render method will show us loading each time we are loading something, renderComments() will return us comments. We get clean and readable code.
render() {
if (isLoading) {
return <div>Loading...</div>
}
return (
<div>
{this.renderComments()}
</div>
);
}

AWS Amplify: onStatusChange then render main page

I am trying to render the main entry point of my application when an auth status change occurs but when I do try to go to the main entry point of my application I get a blank screen. I'm assuming I can't go to a main page if it isn't being called within the render function itself? If so, how do I render the main Page of my app when I established signIn?
index.js
class App extends Component {
/*
constructor(props) {
super(props);
this.state = {
authState: null,
authData: null
}
}
*/
handleAuthStateChange(state) {
alert(state)
if (state === 'signedIn') {
alert('here');
return ( // go To Entry point of app
<ApolloProvider store={store} client={client}>
<AppWithNavigationState/>
</ApolloProvider>
);
}
}
render() {
//const { authState, authData } = this.state;
// const signedIn = (authState === 'signedIn');
return (
<Authenticator hideDefault={true} onStateChange={this.handleAuthStateChange}>
<Login/>
</Authenticator>
);
}
}
export default App = codePush(App);
Login.js
export default class Login extends Component {
render() {
const { authState, onStateChange } = this.props;
if (!['signIn', 'signedOut', 'signedUp'].includes(authState)) {
alert('login')
return null;
}
return (<View style={styles.container}>
<View style={styles.backgroundContainer}>
<Image source={images.icons.LoginImage} style={styles.backgroundImage} />
</View>
<View style={styles.overlay}>
<Button iconLeft light rounded style={styles.facebookLoginButton}>
<Icon style={styles.facebookIcon} name='logo-facebook' />
<Text style={styles.facebookButtonText}>Login with Facebook</Text>
</Button>
<Button rounded bordered light style={styles.loginLaterButton}
onPress={() => onStateChange('signedIn')}>
<Text style={styles.buttonText}>Sign In Test</Text>
</Button>
</View>
</View>
);
}
}
If you resolved it I hope it will help someone else.
I think the following is a better option than using 'onAuthStateChange':
from Amplify dox :
import { Auth } from 'aws-amplify';
Auth.currentAuthenticatedUser({
bypassCache: false // Optional, By default is false. If set to true, this call will send a request to Cognito to get the latest user data
}).then(user => console.log(user))
.catch(err => console.log(err));
You can add your logic within '.then(user => ...' to add route to your protected pages. Also you can redirect to Login page from '.catch(err => '.
If you include above code within a function and call it from 'componentWillReceiveProps' it should be called every time auth status changes.
This is really about rendering and state (and not anything to do with AWS Amplify). First, set up state in your constructor:
constructor(props) {
super(props);
this.state = { authState: '' };
}
Then, your onAuthStateChange() becomes:
onAuthStateChange(newState) {
if (newState === 'signedIn') {
this.setState({ authState: newState });
}
}
Finally, in your render() method, you adjust your rendering so that it does "the right thing" based on your auth state.
render() {
if (this.state.authState === 'signedIn') {
return (<ApolloProvider ...><MyApp/></ApolloProvider>);
} else {
return (<Authenticator .../>);
}
}
You can abstract this away with a HOC as well (the same way the withAuthenticator() HOC from AWS Amplify does it). Basically, the Authenticator gets displayed initially. Once the signedIn state is received, the internal component state is updated and that causes a re-render of the component with the rest of your app.

Categories