How to design the React component properly? - reactjs

Hi I am pretty new to react and I want to achieve something that turns out to be real pain for me and I am not sure what the best solution might be.
I got an application that handles a single keystore as application keystore and the admin is allowed to upload new keystores and merge the entries of these keystores into the application keystore. This results in the following class definition:
export default class ApplicationKeystore extends React.Component
{
constructor(props)
{
super(props);
this.scimResourcePath = "/scim/v2/Keystore";
this.state = {};
this.setAliasSelectionResponse = this.setAliasSelectionResponse.bind(this);
this.onAliasSelectionSuccess = this.onAliasSelectionSuccess.bind(this);
}
setAliasSelectionResponse(resource)
{
let copiedResource = JSON.parse(JSON.stringify(resource));
this.setState({aliasSelectionResponse: copiedResource})
}
onAliasSelectionSuccess(resource)
{
this.setState({newAlias: resource[ScimConstants.CERT_URI].alias})
}
render()
{
return (
<React.Fragment>
<KeystoreUpload scimResourcePath={this.scimResourcePath}
setAliasSelectionResponse={this.setAliasSelectionResponse} />
<AliasSelection scimResourcePath={this.scimResourcePath}
aliasSelectionResponse={this.state.aliasSelectionResponse}
onCreateSuccess={this.onAliasSelectionSuccess} />
<KeystoreEntryList scimResourcePath={this.scimResourcePath}
newAlias={this.state.newAlias} />
</React.Fragment>
)
}
}
My problem now occurs on the last component KeystoreEntryList. The upload succeeds and the selection of entries to be merged works also correctly (just one at a time not several). But if I successfully merge an entry I get a response with the alias of the keystore entry that should now be added to the state of KeystoreEntryList. How do I do this in a clean way. I found several workarounds but all of them are dirty and make the code hard to read...
the important part of the KeystoreEntryListcan be found here:
class KeystoreEntryList extends React.Component
{
constructor(props)
{
super(props);
this.state = {aliases: []};
this.setState = this.setState.bind(this);
this.scimClient = new ScimClient(this.props.scimResourcePath, this.setState);
this.onDeleteSuccess = this.onDeleteSuccess.bind(this);
}
async componentDidMount()
{
let response = await this.scimClient.listResources();
if (response.success)
{
response.resource.then(listResponse =>
{
this.setState({
aliases: new Optional(listResponse.Resources[0]).map(val => val.aliases)
.orElse([])
})
})
}
}
componentDidUpdate(prevProps, prevState, snapshot)
{
// TODO if we delete an alias and add the same again the following if condition prevents adding it again
if (prevProps.newAlias !== this.props.newAlias && this.props.newAlias !== undefined)
{
let aliases = this.state.aliases;
aliases.push(this.props.newAlias);
aliases.sort();
this.setState({aliases: aliases, aliasDeleted: undefined});
}
}
...
}
my first idea was to do it on the componentDidUpdate method but this results in the problem stated out by the TODO. Is there any good way how I might be able to smoothly add the new alias into this component?

You should not mutate the state directly as below
let aliases = this.state.aliases;
aliases.push(this.props.newAlias);
In React when the state is an [] or {} . React just holds the reference of it and check whether the reference change . Since you are pushing a value to the aliases the reference of aliases will not changes which makes react to think nothing has changed and it will not trigger a re-render.
Instead of pushing , you need to create a new copy of the array
const newAliases = [...this.state.alias, this.props.alias];
newAliases.sort;
this.setState({aliases: newAliases, ...})

Related

ReactJS: Reload a new instance of the component?

I hope I am capable to explain my question in a understandable way.
I am very new to React! And I am not familiar with how some things get done yet.
I have a GamePage component. On this page I am rendering a GameRound component.
When the game starts, a new "game round" starts. After the game round has finished, I want a second and then a third game round to start. This means I need to get kind of a "new" GameRound. The GamePage should remain.
While writing this, I got an idea of how this could be achieved: In my gameRoundFinished() event, I could reset the state of the GameRound. But is that already the most elegant and especially the Reactive way doing this?
Thanks in advance.
Some code as requested...
GamePage.js
export class GamePage extends React.Component {
constructor(props) {
super(props);
// Set game configuration and data
this.state = {
gameId: props.match.params.gameId,
settings: {
categories: ['Stadt', 'Land', 'Fluss'],
countdown: 5
},
activePlayers: {},
game: null
};
// Open socket
this.socket = openSocket('http://localhost:3000');
this.socket.emit('join', this.state.gameId);
this.socket.on('updatePlayers', players => {
this.state.activePlayers = players;
this.setState(this.state);
});
this.socket.on('startGame', () => {
this.state.game = {
rounds: []
};
this.state.game.rounds.push({
});
this.setState(this.state);
});
}
onClick = () => {
this.socket.emit('ready');
}
render() {
if (this.state.game) {
return (
<GameRound socket={this.socket} config={this.state}></GameRound>
);
} else {
return (
<div>Currently {this.state.activePlayers.length}/3
<Button onClick={this.onClick}>klick</Button></div>
);
}
}
}
GameRound.js
export class GameRound extends React.Component {
// too much code that is irrelevant for the question
render() {
return ...
}
}
A very simple way to "reset" a component (and re-run the constructor) is to use the key prop.
See https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key
In your case: be sure to provide a new key at every round change: something like:
<GameRound key={roundId} socket={this.socket} config={this.state}></GameRound>
Before other things it is necessary to clean the code:
To call functions together with synthetic events in the render method:
<Button onClick={this.onClickEvent()}>click</Button></div>
To use this.setState({ a: 123 }) for changing this.state object, but not to use this.state itself for changing the state.
Good example:
this.setState({ activePlayers: players });
Bad examples
this.setState(this.state);
this.state.game = { rounds: [] };
this.state.game.rounds.push({ ... });
You may want to use socket.on() function to change your state as follows:
socket.on(){
...
this.setState({ activePlayers: players });
}
<Button onClick={this.socket.on()}>click</Button></div>
One solution is to design GameRound so that it accepts props to determine its behavior. Then to "reset", you just pass the appropriate values for the props to set it to the new round.

React.js, correct way to iterate inside DOM

Im new in ReactJS...
I have a project with the following class components structure:
index.js
--app
--chat
--header
--left
--right
In the chat.js component, I make a google search with the api to retrieve images based on specific keyword... My intuitive solution was:
this.client.search("cars")
.then(images => {
for(let el of images) {
ReactDOM.render(<img src="{{el.url}}" syle="{{width: '100%'}}" />, document.querySelector('#gimages'));
}
});
It is correct? Or I may to use Components with stored states with flux (redux)?
Perhaps a simpler more conventional use of react would achieve what your require?
You could follow a pattern similar to that shown below to achieve what you require in a more "react-like" way:
class Chat extends React.Component {
constructor(props) {
super(props)
this.state = { images : [] } // Set the inital state and state
// model of YourComponent
}
componentDidMount() {
// Assume "client" has been setup already, in your component
this.client.search("cars")
.then(images => {
// When a search query returns images, store those in the
// YourComponent state. This will trigger react to re-render
// the component
this.setState({ images : images })
});
}
render() {
const { images } = this.state
// Render images out based on current state (ie either empty list,
// no images, or populated list to show images)
return (<div>
{
images.map(image => {
return <img src={image.url} style="width:100%" />
})
}
</div>)
}
}
Note that this is not a complete code sample, and will require you to "fill in the gaps" with what ever else you have in your current Chat component (ie setting up this.client)
This is not the way you should go, you don't need to use ReactDOM.render for each item. Actually, you don't need to use ReactDOM.render at all. In your component you can use a life-cycle method to fetch your data, then set it to your local state. After getting data you can pass this to an individual component or directly render in your render method.
class Chat extends React.Component {
state = {
images: [],
}
componentDidMount() {
this.client.search( "cars" )
.then( images => this.setState( { images } ) );
}
renderImages = () =>
this.state.images.map( image => <Image key={image.id} image={image} /> );
render() {
return (
<div>{this.renderImages()}</div>
);
}
}
const Image = props => (
<div>
<img src={props.image.url} syle="{{width: '100%'}}" />
</div>
);
At this point, you don't need Redux or anything else. But, if you need to open your state a lot of components, you can consider it. Also, get being accustomed to using methods like map, filter instead of for loops.

ReactJS: why is pushing value into this.state array causing type error?

I tried to push a value into a state array but I get an issue TypeError: Cannot read property 'state' of undefined at this.state.rows.push(a);
Know why? I am trying to push a new value into the array after i click a button.
App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
constructor() {
super();
this.state = {
name: '',
rows: ['hello',<p>gfdfg</p>,'mello']
}
}
handle(e){
e.preventDefault();
var a = "h";
this.state.rows.push(a);
alert("hello");
}
render() {
return (
<div className="App">
Button<br/>
<input type="submit" id="black" onClick={this.handle}/><br/>
<p>{this.state.rows}</p>
</div>
);
}
}
export default App;
There are couple of things that are wrong here:
you should NEVER change the state directly:
This is a big No No:
this.state.rows.push(a);
instead you should do something like this:
this.setState({ rows : [...this.state.rows, a] })
or without ES6:
const newArray = this.state.rows.slice();
newArray.push(a);
this.setState({ rows: newArray })
You should always replace the state with a new one.
this in a react component is not what you think it is, in order to make it work you can do one of two things:
a. change your method to an arrow function:
handle = (e) => {
e.preventDefault();
var a = "h";
this.state.rows.push(a);
alert("hello");
}
b. bind this to the method:
constructor() {
super();
this.state = {
name: '',
rows: ['hello',<p>gfdfg</p>,'mello']
}
this.handle = this.handle.bind(this);
}
the method handle does not have access to the context of the class i.e this; consider writing it as a fat arrow function
// class definition
handle = () => {
e.preventDefault();
var a = "h";
this.state.rows.push(a);
alert("hello");
}
render() {
// render logic
}
Having said this, mutating the state is not a good idea, consider using setState if you want your component to re-render as a result of state change
handle = () => {
e.preventDefault();
let { rows } = this.state;
var a = "h";
rows.push(a);
this.setState({
rows,
});
}
You are doing wrong, you have to use setState() method to push the value in the array:
handle = (e) => {
e.preventDefault();
var a = "h";
let tempRows = [...this.state.rows];
tempRows.push(a)
this.setState({rows:tempRows})
alert("hello");
}
You have two problems.
Event handlers require 'this' to be bound: https://reactjs.org/docs/handling-events.html So following this, you must either write: this.handle = this.handle.bind(this) in your contructor, or change handler to arrow function, if your build process supports transpilation of class fields.
React component will only update if component props change, or component state changes. Which is done by comparing references. In your case, when you push to the array, you are mutating the state, so the new reference is never created, and component does not re-render. If you want to verify that, just put console.log(this.state.rows) below this.state.rows.push(a) and you'll see that the array has received the new value, but component does not represent it. You need to use this.setState to create a new reference for your state, like so: this.setState({ rows: [...this.state.rows, a] })
Another way of returning a new array from the current array with additional elements and then pushing to state is to use the concat method.
Example :
this.setState({ users: this.state.users.concat(<Additonal Items Here>)}

How to manage read-only files that don't have to be updated using React+Flux

I'm using React-Flux on an app I'm working on and I've hit a wall, so I have to make sure that so far I'm using everything properly.
I have a json file that is only for reading purposes. It should never change dynamically. I'm getting the file data from an Ajax request I'm performing in my AppActions file. So far so good. Now I'm trying to use the stored data. My question is this: In the AppStore file should I add an emitChange event to the corresponding case? How should I manage read-only files?
This whole thing came up when I tried to use the data in a component I was building and if I wasn't using an event listener inside the componentWillMount and componentWillUnmount functions I couldn't get the stored data every time I refreshed the page.
const AppStore = new AppStoreClass();
AppStore.dispatchToken = AppDispatcher.register(action =>{
switch(action.actionType){
case AppConstants.GET_NAMES:
setNames(action.names);
AppStore.emitChange();
break;
}
});
export default AppStore;
This is the Nameday component where I use the read-only data.
class Nameday extends Component {
constructor(){
super();
this.state = {
saints: AppStore.getNames(),
}
this.onChange = this.onChange.bind(this);
}
componentWillMount(){
AppStore.addChangeListener(this.onChange);
}
componentDidMount(){
AppActions.getNames();
}
onChange(){
this.setState({
saints: AppStore.getNames(),
});
}
componentWillUnmount(){
AppStore.removeChangeListener(this.onChange);
}
render() {
let dates = [];
this.state.saints.map((saint, i) => {
saint.names.map((name, j) => {
if (name === firstName) {
dates.push(saint.date);
}
return dates;
});
return null;
});
let option = ....
return (
option
);
}
}

componentDidMount() not being called when react component is mounted

I've been attempting to fetch some data from a server and for some odd reason componentDidMount() is not firing as it should be. I added a console.log() statement inside of componentDidMount() to check if it was firing. I know the request to the server works as it should As I used it outside of react and it worked as it should.
class App extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
obj: {}
};
};
getAllStarShips () {
reachGraphQL('http://localhost:4000/', `{
allStarships(first: 7) {
edges {
node {
id
name
model
costInCredits
pilotConnection {
edges {
node {
...pilotFragment
}
}
}
}
}
}
}
fragment pilotFragment on Person {
name
homeworld { name }
}`, {}). then((data) => {
console.log('getALL:', JSON.stringify(data, null, 2))
this.setState({
obj: data
});
});
}
componentDidMount() {
console.log('Check to see if firing')
this.getAllStarShips();
}
render() {
console.log('state:',JSON.stringify(this.state.obj, null, 2));
return (
<div>
<h1>React-Reach!</h1>
<p>{this.state.obj.allStarships.edges[1].node.name}</p>
</div>
);
}
}
render(
<App></App>,
document.getElementById('app')
);
The issue here is that the render method is crashing, because the following line is generating an error
<p>{this.state.obj.allStarships.edges[1].node.name}</p>
Fix this to not use this.state.obj.allStarships.edges[1].node.name directly, unless you can guarantee that each receiver is defined.
Check your component's key
Another thing that will cause this to happen is if your component does not have a key. In React, the key property is used to determine whether a change is just new properties for a component or if the change is a new component.
React will only unmount the old component and mount a new one if the key changed. If you're seeing cases where componentDidMount() is not being called, make sure your component has a unique key.
With the key set, React will interpret them as different components and handle unmounting and mounting.
Example Without a Key:
<SomeComponent prop1={foo} />
Example with a Key
const key = foo.getUniqueId()
<SomeComponent key={key} prop1={foo} />
Also check that you don't have more than one componentDidMount if you have a component with a lot of code. It's a good idea to keep lifecycle methods near the top after the constructor.
I encountered this issue (componentDidMount() not being called) because my component was adding an attribute to the component state in the constructor, but not in the Component declaration. It caused a runtime failure.
Problem:
class Abc extends React.Component<props, {}> {
this.state = { newAttr: false }; ...
Fix:
class Abc extends React.Component<props, {newAttr: boolean}> {
this.state = { newAttr: false }; ...

Resources