React Redux-a child component does not re-render on state change - javascript

The architecture is like this: I have a plotMap component, which gets a list of plots from state and maps them to a bunch of plotMarker components, which return polygons/markers, based on map zoom (also read from state). If a given plot is selected for editing, the plotMarker component returns a plotPolygon component which is editable. When a user saves the edited plotPolygon component, this updates the corresponding plot in the state plot list.
Problem: the plotMarker's polygon, which is displayed as soon as the edited plotPolygon component is successfully saved, is not updated with the new shape, but keeps the old. Only when one zooms out, and the plotMarker renders its marker component, and zooms back in, and the plotMarker renders its polygon component again, is the new shape displayed.
Could this be due to a lag inside the app? How can I make the plotMarker display the new polygon as soon as it is successfully saved?
plotMap component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import { Map, TileLayer, LayersControl, MapControl } from 'react-leaflet';
import { GoogleLayer } from './GoogleLayer';
import { geolocated } from 'react-geolocated';
import 'leaflet-geocoder-mapzen';
import SearchBox from './searchBox';
import Control from 'react-leaflet-control';
import { centroid } from '#turf/turf';
import PlotMarker from './plotMarker';
const { BaseLayer } = LayersControl;
const key = 'key';
const hybrid = 'HYBRID';
const terrain = 'TERRAIN';
const road = 'ROADMAP';
const satellite = 'SATELLITE';
const centerLat = props => {
if (
props.isGeolocationAvailable &&
props.isGeolocationEnabled &&
props.coords
) {
return props.coords.latitude;
}
return 32.11;
};
const centerLong = props => {
if (
props.isGeolocationAvailable &&
props.isGeolocationEnabled &&
props.coords
) {
return props.coords.longitude;
}
return 34.963;
};
const mapCenterPoint = props => {
if (props.plots && (props.selectedPlot || props.plotBeingEdited)) {
let ourPlot = props.plots.filter(
plot => plot._id === (props.selectedPlot || props.plotBeingEdited)
)[0];
try {
let center = centroid(ourPlot.feature).geometry.coordinates.reverse();
return { center: center, zoom: 16 };
} catch (e) {
console.log(e);
}
}
return { center: [centerLat(props), centerLong(props)], zoom: 8 };
};
export class PlotMap extends Component {
markers = props => {
if (props.plots) {
return (
<div>
{(props.filteredPlots || props.plots).map(
plot =>
plot &&
plot.feature &&
plot._id && (
<PlotMarker
key={plot._id}
id={plot._id}
name={plot.name}
geoJSON={plot.feature}
/>
)
)}
</div>
);
}
};
render() {
return (
<div
className="col-sm-8 m-auto p-0 flex-column float-right"
style={{ height: `85vh` }}>
<Map
center={mapCenterPoint(this.props).center}
zoom={mapCenterPoint(this.props).zoom}
zoomControl={true}
onZoomend={e => {
this.props.setZoomLevel(e.target.getZoom());
}}
onMoveEnd={e => {
this.props.setMapCenter(e.target.getCenter());
}}>
<LayersControl position="topright">
<BaseLayer name="Google Maps Roads">
<GoogleLayer googlekey={key} maptype={road} />
</BaseLayer>
<BaseLayer name="Google Maps Terrain">
<GoogleLayer googlekey={key} maptype={terrain} />
</BaseLayer>
<BaseLayer checked name="Google Maps Hybrid">
<GoogleLayer
googlekey={key}
maptype={hybrid}
libraries={['geometry', 'places']}
/>
</BaseLayer>
</LayersControl>
<SearchBox postion="bottomright" />
{this.markers(this.props)}
</Map>
</div>
);
}
}
function mapStateToProps(state) {
return {
filteredPlots: state.plots.filteredPlots,
plots: state.plots.plots,
selectedPlot: state.plots.selectedPlot,
mapCenter: state.plots.mapCenter
};
}
export default geolocated({
positionOptions: {
enableHighAccuracy: false
},
userDecisionTimeout: 5000,
suppressLocationOnMount: false
})(connect(mapStateToProps, actions)(PlotMap));
plotMarker component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import { Marker, Popup, GeoJSON } from 'react-leaflet';
import { centroid } from '#turf/turf';
import PlotPolygon from './plotPolygon';
const position = geoJSON => {
return centroid(geoJSON).geometry.coordinates.reverse();
};
export class PlotMarker extends Component {
render() {
const {
id,
name,
geoJSON,
zoomLevel,
selectedPlot,
plotBeingEdited
} = this.props;
const markerPosition = position(geoJSON);
let style = () => {
return {
color: 'blue'
};
};
if (selectedPlot === id) {
style = () => {
return {
color: 'red'
};
};
}
if (zoomLevel > 14 && plotBeingEdited === id) {
return <PlotPolygon id={id} geoJSON={geoJSON} />;
} else if (zoomLevel > 14) {
return (
<GeoJSON
id={id}
data={geoJSON}
style={style}
onClick={() => {
this.props.selectPlot(id);
}}
/>
);
}
return (
<Marker
id={id}
className="marker"
position={markerPosition}
onClick={() => {
this.props.selectPlot(id);
}}>
<Popup>
<span>{name}</span>
</Popup>
</Marker>
);
}
}
function mapStateToProps(state) {
return {
selectedPlot: state.plots.selectedPlot,
plotBeingEdited: state.plots.plotBeingEdited,
zoomLevel: state.plots.zoomLevel,
plots: state.plots.plots,
filteredPlots: state.plots.filteredPlots
};
}
export default connect(mapStateToProps, actions)(PlotMarker);
plotPolygon component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import { Polygon, FeatureGroup } from 'react-leaflet';
import { EditControl } from 'react-leaflet-draw';
const positions = props => {
return props.geoJSON.geometry.coordinates[0].map(a => [a[1], a[0]]);
};
export class PlotPolygon extends Component {
render() {
const { id, geoJSON } = this.props;
return (
<FeatureGroup>
<EditControl
position="topright"
onEdited={e => {
e.layers.eachLayer(a => {
this.props.updatePlot({
id: id,
feature: a.toGeoJSON()
});
});
}}
edit={{ remove: false }}
draw={{
marker: false,
circle: false,
rectangle: false,
polygon: false,
polyline: false
}}
/>
<Polygon positions={[positions(this.props)]} />;
</FeatureGroup>
);
}
}
function mapStateToProps(state) {
return { plots: state.plots.plots, filteredPlots: state.plots.filteredPlots };
}
export default connect(mapStateToProps, actions)(PlotPolygon);

Original Solution:
Do you need to reset your zoom level after you exit the edit mode? Your logic is very tightly coupled between mapCenterPoint and the if(zoomLevel... portion of plotMarker. I'm wondering if you may have a state that is causing this to always evaluate true after editing (and until manually zooming): props.plots && (props.selectedPlot || props.plotBeingEdited)
Thoughts on Refactoring:
to figure out a more elegant way
Option 1: HOCs
Higher Order Components (or HOCs) might be a great approach for this. You have an example of a HOC already in your code:
export default connect(mapStateToProps, actions)(PlotMarker);
HOCs are really just functions that return something useful to you in the React world. As such, they're a great form of code reuse. connect is a HOC that returns a function in this scenario, but HOCs are often used to return components. So you may have a HOC like plotMarker(zoomLevel, beingEdited) that performs the logic itself to determine which type of marker component to render.
Check out React's Higher Order Component Documentation for more.
Option 2: Single Return Statement
I've always been a fan of using a single return statement for the render method. I haven't dug in super deep to how React handles diffs, but I'm pretty sure from my experience that it responds better this way - so it might remove the need for you to twitch the zoom levels to reset it.
How I would perform all of PlotMarker's logic inside one return:
export class PlotMarker extends Component{
render(){
const {
id,
name,
geoJSON,
zoomLevel,
selectedPlot,
plotBeingEdited
} = this.props;
const markerPosition = position(geoJSON);
let style = () =>{
return {
color: 'blue'
};
};
if(selectedPlot === id){
style = () =>{
return {
color: 'red'
};
};
}
return (
<div>
{
zoomLevel > 14 && plotBeingEdited === id &&
<PlotPolygon id={id} geoJSON={geoJSON}/> ||
zoomLevel > 14 &&
<GeoJSON
id={id}
data={geoJSON}
style={style}
onClick={() =>{
this.props.selectPlot(id);
}}
/> ||
<Marker
id={id}
className="marker"
position={markerPosition}
onClick={() => {
this.props.selectPlot(id);
}}>
<Popup>
<span>{name}</span>
</Popup>
</Marker>
}
</div>
);
}
}

Related

ReactNative) Questions about calling a function that references data within the render()

The program flows as follows:
Home.js => Add.js => Detail.js => Repeat...
Home.js lists the posts.
Add.js is a page for entering the data required for a post. Move through stack navigation from Home.js
Detail.js will take over the data entered in Add.js as a parameter, show you the information of the post to be completed, and press the Done button to move to Home.js.
Home.js contains posts that have been completed.
If a post is created, eatObject is assigned an object from Detail.js.
Const { eatObject } = this.props.route?params || {};
In Home.js, state has an array of objects toEats.
Add eatObject to the object list
{Object.values(toEats).map(toEat =toEat.id toEat.idToEat key={toEat.id} {...toEat} deleteToEat={this._deleteToEat} />)}
Currently, the refresh button updates the 'Home.js' list whenever a post is added. But I want the list of posts to be updated automatically whenever 'Home.js' is uploaded.
I tried to call '_pushToEat' in 'componentDidMount', but 'eatObject' exists in 'render' so it seems impossible to hand it over to parameters.
I want to automatically call '_pushTooEat' when the components of 'Home.js' are uploaded.
Home.js
import React from "react";
import { View, Text, Button } from "react-native";
import ToEat from "./ToEat"
export default class Home extends React.Component {
state = {
toEats:{}
}
render() {
const { toEats } = this.state;
const { eatObject } = this.props.route?.params || {};
// error
// if (eatObject) {
// () => this._pushToEat(eatObject)
// }
return (
<View>
{Object.values(toEats).map(toEat => <ToEat key={toEat.id} {...toEat} deleteToEat={this._deleteToEat} />)}
<Button title="refresh" onPress={()=>this._pushToEat(eatObject)}/>
<View>
<Button title="Add" onPress={() => this.props.navigation.navigate("Add")} />
</View>
</View>
);
}
_deleteToEat = () => {
}
_pushToEat = (eatObject) => {
console.log("function call!")
console.log(eatObject)
this.setState(prevState => {
const newToEatObject = eatObject
const newState = {
...prevState,
toEats: {
...prevState.toEats,
...newToEatObject
}
}
return { ...newState };
})
}
}
Detail.js
import React from "react";
import { View, Text, Button } from "react-native";
import uuid from 'react-uuid'
import { connect } from 'react-redux';
import { inputId } from '../src/reducers/infoReducer';
class Detail extends React.Component {
render() {
const { name, dosage, note } = this.props.route.params;
return (
<View>
<Text>name: {name}</Text>
<Text>dosage: {dosage}</Text>
<Text>note: {note}</Text>
<Button
title="Done"
onPress={() =>
this.props.navigation.navigate(
"Home", {
eatObject: this._createToEat(uuid(), name, dosage)
})
}
/>
</View>
)
}
_createToEat = (id, name, dosage) => {
const ID = id
inputId(id)
const newToEatObject = {
[ID]: {
id: ID,
name: name,
dosage: dosage,
}
}
return newToEatObject;
}
}
function mapStateToProps(state) {
return {
state: state.infoReducer
};
}
function matchDispatchToProps(dispatch) {
return {
inputId: (id) => {
dispatch(inputId(id));
},
}
}
export default connect(mapStateToProps, matchDispatchToProps)(Detail);
I want your advice.
Thank you

Dropdown Value not passing from one component to the other - React JS

I'm trying to create a web-app with the front end in React and backend in Flask. I have a dropdown which gets populated by the flask JSON which is basically a list of companies. All total there are around 5 components, the first one is the App.js, second is the CompanySelection.js, the third one is the Chart.js where I want to return my graphs and all.
So in theCompanySelection.js when I change the dropdown selection the updated company name does not go into Charts.js, i.e. the other component. I guess when this part gets solved similarly I can pass the values from one component to the other easily.
These are my three code files:
App.js
import React from "react";
import { CompanyContextProvider } from "./context";
import Dropdown from 'react-dropdown';
import 'react-dropdown/style.css';
import Header from "./Header";
import CompanySelection from "./CompanySelection/CompanySelection.js";
import Charts from "./Charts/Chart.js";
import axios from 'axios';
class App extends React.Component{
state = {
companies: [],
firstCompany: {},
firstCompanyName: ''
};
componentDidMount() {
fetch('http://127.0.0.1:5001/algo/loc')
.then(res => res.json())
.then(data => {
this.setState({companies: data,
firstCompany: data[0],
firstCompanyName: data[0].value}, () =>
console.log(this.state.companies, this.state.firstCompany, this.state.firstCompanyName));
console.log('')
}).catch(function (error) {
console.log(error);
});
}
selectedValueHandler = (selectedValue) => {
this.setState({
firstCompanyName: selectedValue
})
}
render() {
const { selectedValue } = this.state.firstCompanyName;
console.log('change value',selectedValue)
return (
<div className="app">
<Header/>
<CompanySelection companies= {this.state.companies} selectedCompany={this.state.firstCompany} setSelectedCompany={this.state.firstCompanyName} selectedValueHandler = {this.selectedValueHandler}/>
<Charts companies= {this.state.companies} selectedCompany={this.state.firstCompany} setSelectedCompany={selectedValue}/>
</div>
);
}
} ;
export default App;
CompanySelection.js
import { h, render, Component} from 'preact';
import style from './style.css';
import { useContext } from "preact/hooks";
import { CompanyContext } from "../context";
class CompanySelection extends Component {
constructor(props)
{
super(props);
}
render(_, { value }) {
const companies = this.props.companies;
const selectedCompany = this.props.selectedCompany;
const setSelectedCompany = this.props.setSelectedCompany;
var onChange = (e) =>{
console.log("In on change");
this.setState({ value: e.target.value });
const setSelectedCompany = e.target.value;
console.log("Selected", e.target.value);
const companies = this.props.companies;
const selectedCompany = this.props.selectedCompany;
this.props.selectedValueHandler(e.target.value);
}
if (typeof companies !== 'undefined')
{
var options = companies.map((comp) =>
<option
key={comp.label}
value={comp.value}
>
{comp.label}
</option>
);
}
else {
var options = [{value: 'A', label: 'B'}].map((comp) =>
<option
key={comp.label}
value={comp.value}
>
{comp.label}
</option>
);
}
return (
<fragment class={style.fragment}>
<label class={style.label}> Company </label>
<select value={value} onChange={onChange} class={style.dropdown}>
{options}
</select>
</fragment>
);
}
}
render(<CompanySelection />, document.body);
export default CompanySelection;
Chart.js
import { h, render, Component } from 'preact';
import style from './style.css';
import { VictoryChart, VictoryLine, VictoryScatter, VictoryLabel} from 'victory';
import { useContext } from "preact/hooks";
import { CompanyContext } from "../context";
class Charts extends Component {
constructor(props)
{
super(props);
}
render(_, { value }) {
const companies = this.props.companies;
const selectedCompany = this.props.selectedCompany;
const setSelectedCompany = this.props.setSelectedCompany;
console.log('list of companies chart', companies)
console.log('chart input', setSelectedCompany)
if (typeof selectedCompany !== 'undefined') {
var comp = selectedCompany;
}
else {
var comp = '';
}
console.log("comp", comp);
return (
<fragment>
<div class={style.chart}>
<VictoryChart domain={[0, 10]}>
<VictoryLabel text={comp} x={225} y={30} textAnchor="middle"/>
<VictoryLine
style={{ data: { stroke: "blue", strokeWidth: 3 } }}
y={(d) => d.x}
/>
<VictoryScatter
symbol="star"
size={8}
style={{ data: { fill: "red" }}}
data={[{ x: 5, y: 5 }]}
/>
<VictoryScatter
symbol="circle"
size={8}
style={{ data: { fill: "red" }}}
data={[{ x: 7, y: 7 }]}
/>
</VictoryChart>
</div>
</fragment>
);
}
}
render(<Charts />, document.body);
export default Charts;
I have taken reference of this code from this stackoverflow post: How to pass data from one component to another component in onchange using React js
However when I see the output of this line of code: const { selectedValue } = this.state.firstCompanyName;
console.log('change value',selectedValue)
I get that change value undefined, which means the values are not getting passed on. I'm very new to react and haven't been able to solve this yet. Any help is much appreciated.
P.S. The components pass on well to the <CompanySelection..../>
However when I see the output of this line of code: const { selectedValue } = this.state.firstCompanyName;
console.log('change value',selectedValue)
It will be undefined, here you are trying to destructure selectedValue from a string firstCompanyName, as long as this.state.firstCompanyName is not an object with a key named selectedValue it will be undefined. Instead do this.
const selectedValue = this.state.firstCompanyName;
console.log('change value',selectedValue)`

How to pass my onSucceeded() function to the parent component?

I have 2 components OptinPage (parent) and TermsOfServices (child). Optin Page is only used for rendering the TermsOfServices component, which can be reused elsewhere in the application. I want to use my onSucceeded () function from my child component to my parent component. I don't see how to do it at all. Currently the result is such that when I click on the button that validates the TermsOfServices it seems to be an infinite loop, it goes on and on without closing my popup. Before I split my TermsOfServices component into a reusable component it worked fine. Before, all content was gathered in OptinPage. Any ideas? Thanks in advance
my TermsOfServices component:
import API from 'api';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
Block,
BlockTitle,
Col,
Fab,
Icon,
Preloader,
} from 'framework7-react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-refetch';
import ReactHtmlParser from 'react-html-parser';
class TermsOfServices extends PureComponent {
static propTypes = {
agreeTosFunc: PropTypes.func.isRequired,
agreeTos: PropTypes.object,
onSucceeded: PropTypes.func,
tos: PropTypes.object.isRequired,
};
static contextTypes = {
apiURL: PropTypes.string,
loginToken: PropTypes.string,
userId: PropTypes.string,
};
static defaultProps = {
agreeTos: {},
onSucceeded: () => {},
};
state = {
currentTos: -1,
};
componentDidUpdate(prevProps) {
const {
agreeTos,
onSucceeded,
tos,
} = this.props;
const { currentTos } = this.state;
/* Prepare for first tos after receiving all of them */
if (
prevProps.tos.pending &&
tos.fulfilled &&
tos.value.length &&
currentTos < 0
) {
this.setState({ currentTos: 0 });
}
/* When sending ToS agreement is done */
if (
prevProps.agreeTos.pending &&
agreeTos.fulfilled
) {
onSucceeded();
}
}
handleNext = () => {
const { agreeTosFunc, tos } = this.props;
const { currentTos: currentTosId } = this.state;
const termsOfServices = tos.value;
const done = currentTosId + 1 === termsOfServices.length;
this.setState({ currentTos: currentTosId + 1 });
if (done) {
agreeTosFunc(termsOfServices.map((v) => v._id));
}
};
render() {
const { tos } = this.props;
const { currentTos: currentTosId } = this.state;
const termsOfServices = tos.value;
const currentTermsOfServices = termsOfServices && termsOfServices[currentTosId];
const loaded = termsOfServices && !tos.pending && tos.fulfilled;
const htmlTransformCallback = (node) => {
if (node.type === 'tag' && node.name === 'a') {
// eslint-disable-next-line no-param-reassign
node.attribs.class = 'external';
}
return undefined;
};
return (
<div>
{ (!loaded || !currentTermsOfServices) && (
<div id="
optin_page_content" className="text-align-center">
<Block className="row align-items-stretch text-align-center">
<Col><Preloader size={50} /></Col>
</Block>
</div>
)}
{ loaded && currentTermsOfServices && (
<div id="optin_page_content" className="text-align-center">
<h1>
<FormattedMessage id="press_yui_tos_subtitle" values={{ from: currentTosId + 1, to: termsOfServices.length }} />
</h1>
<BlockTitle>
{ReactHtmlParser(
currentTermsOfServices.title,
{ transform: htmlTransformCallback },
)}
</BlockTitle>
<Block strong inset>
<div className="tos_content">
{ReactHtmlParser(
currentTermsOfServices.html,
{ transform: htmlTransformCallback },
)}
</div>
</Block>
<Fab position="right-bottom" slot="fixed" color="pink" onClick={() => this.handleNext()}>
{currentTosId + 1 === termsOfServices.length &&
<Icon ios="f7:check" aurora="f7:check" md="material:check" />}
{currentTosId !== termsOfServices.length &&
<Icon ios="f7:chevron_right" aurora="f7:chevron_right" md="material:chevron_right" />}
</Fab>
{currentTosId > 0 && (
<Fab position="left-bottom" slot="fixed" color="pink" onClick={() => this.setState({ currentTos: currentTosId - 1 })}>
<Icon ios="f7:chevron_left" aurora="f7:chevron_left" md="material:chevron_left" />
</Fab>
)}
</div>
)}
</div>
);
}
}
export default connect.defaults(new API())((props, context) => {
const { apiURL, userId } = context;
return {
tos: {
url: new URL(`${apiURL}/tos?outdated=false&required=true`),
},
agreeTosFunc: (tos) => ({
agreeTos: {
body: JSON.stringify({ optIn: tos }),
context,
force: true,
method: 'PUT',
url: new URL(`${apiURL}/users/${userId}/optin`),
},
}),
};
})(TermsOfServices);
My OptIn Page :
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
Link,
NavRight,
Navbar,
Page,
Popup,
} from 'framework7-react';
import { FormattedMessage, intlShape } from 'react-intl';
import './OptInPage.scss';
import TermsOfServices from '../components/TermsOfServices';
class OptinPage extends PureComponent {
static propTypes = {
logout: PropTypes.func.isRequired,
opened: PropTypes.bool.isRequired,
};
static contextTypes = {
intl: intlShape,
logout: PropTypes.func,
};
render() {
const { opened, logout } = this.props;
const { intl } = this.context;
const { formatMessage } = intl;
return (
<Popup opened={opened} className="demo-popup-swipe" tabletFullscreen>
<Page id="optin_page">
<Navbar title={formatMessage({ id: 'press_yui_tos_title' })}>
<NavRight>
<Link onClick={() => logout()}>
<FormattedMessage id="press_yui_comments_popup_edit_close" />
</Link>
</NavRight>
</Navbar>
</Page>
<TermsOfServices onSucceeded={this.onSuceeded} />
</Popup>
);
}
}
export default OptinPage;
Just add the data you want the parent to be supplied with in the child component (when it is hit) and then handle the data passed to the parent in the function that you pass in onSuccess.
This will roughly look like this:
const {useState, useEffect} = React;
function App(){
return <Child onSuccess={(data)=>{console.log(data)}}/>;
}
function Child({onSuccess}){
return <div>
<button
onClick={()=>onSuccess("this is the data from the child component")}>
Click to pass data to parent
</button>
</div>;
}
ReactDOM.render(<App/>,document.getElementById('app'));
#element {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='app'></div>
<div id="element">
<div>node 1</div>
<div>node 2</div>
</div>
to access to parent method or attribute you should use super,
for call to the parent constructor
super([arguments]);
for call parent method
super.parentMethod(arguments);
I recommend create a method on child class and then call the parent method, not directly
for more information take a look on this
https://www.w3schools.com/jsref/jsref_class_super.asp

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 the last index in an array when I click (in an array that continues to be updated)

I am near the end of creating my application.
So it is for banks accounts where they ask you to give the first letter of your password, then for example fourth, etc.
I'm tired of counting on my own so I created this app.
But there is the last bug that I don't know how to fix.
So when I press "1" I get "1 - H", and then when I press "4" I want to get:
"1 - H" (clicked before)
"4 - X" (clicked just now)
but instead, I get:
"4 - X" (clicked just now)
"4 - X" (clicked just now)
So it is caused by the way handleResults() function works inside my Input component, but for now it is my only concept how to approach this...
import React, { Component } from 'react';
import TextField from 'material-ui/TextField';
import './style.css';
import Buttons from '../Buttons';
import Results from '../Results';
class Input extends Component {
constructor(props) {
super(props);
this.state = {
password: 'Hh9Xzke2ayzcEUPHuIfS',
selectedButtons: [],
};
this.handleButtonSelectTwo = this.handleButtonSelectTwo.bind(this);
}
handleInputChange(pass) {
this.setState({ password: pass });
}
handleButtonSelectTwo(selected) {
this.setState({
selectedButtons: [...this.state.selectedButtons, selected],
});
}
handleResults() {
return this.state.selectedButtons.map(el => (
<Results key={el} appState={this.state} />
));
}
render() {
return (
<div>
<div className="Input-textfield">
<TextField
hintText="Paste your password here to begin"
value={this.state.password}
onChange={event => this.handleInputChange(event.target.value)}
/>
</div>
<div>
<Buttons
handleButtonSelectOne={this.handleButtonSelectTwo}
array={this.state.password.length}
/>
{this.handleResults()}
</div>
</div>
);
}
}
export default Input;
and here is Results component code:
import React, { Component } from 'react';
import _ from 'lodash';
import Avatar from 'material-ui/Avatar';
import List from 'material-ui/List/List';
import ListItem from 'material-ui/List/ListItem';
import './style.css';
const style = {
avatarList: {
position: 'relative',
left: -40,
},
avatarSecond: {
position: 'relative',
top: -40,
left: 40,
},
};
class Results extends Component {
resultsEngine(arg) {
const { selectedButtons, password } = this.props.appState;
const passwordArray = password.split('').map(el => el);
const lastSelectedButton = _.last(selectedButtons);
const passwordString = passwordArray[_.last(selectedButtons) - 1];
if (arg === 0) {
return lastSelectedButton;
}
if (arg === 1) {
return passwordString;
}
return null;
}
render() {
if (this.props.appState.selectedButtons.length > 0) {
return (
<div className="test">
<List style={style.avatarList}>
<ListItem
disabled
leftAvatar={<Avatar>{this.resultsEngine(0)}</Avatar>}
/>
<ListItem
style={style.avatarSecond}
disabled
leftAvatar={<Avatar>{this.resultsEngine(1)}</Avatar>}
/>
</List>
</div>
);
}
return <div />;
}
}
export default Results;
Anyone has an idea how should I change my code inside handleResults() function to achieve my goal? Any help with solving that problem will be much appreciated.
Buttons component code:
import React from 'react';
import OneButton from '../OneButton';
const Buttons = props => {
const arrayFromInput = props.array;
const buttonsArray = [];
for (let i = 1; i <= arrayFromInput; i++) {
buttonsArray.push(i);
}
const handleButtonSelectZero = props.handleButtonSelectOne;
const allButtons = buttonsArray.map(el => (
<OneButton key={el} el={el} onClick={handleButtonSelectZero} />
));
if (arrayFromInput > 0) {
return <div>{allButtons}</div>;
}
return <div />;
};
export default Buttons;
And OneButton code:
import React, { Component } from 'react';
import RaisedButton from 'material-ui/RaisedButton';
const style = {
button: {
margin: 2,
padding: 0,
minWidth: 1,
},
};
class OneButton extends Component {
constructor() {
super();
this.state = { disabled: false };
}
handleClick() {
this.setState({ disabled: !this.state.disabled });
this.props.onClick(this.props.el);
}
render() {
return (
<RaisedButton
disabled={this.state.disabled}
key={this.props.el}
label={this.props.el}
style={style.button}
onClick={() => this.handleClick()}
/>
);
}
}
export default OneButton;
In your resultsEngine function in the Results component you are specifying that you always want the _.last(selectedButtons) to be used. This is what it is doing, hence you always see the last button clicked. What you actually want is the index of that iteration to show.
const lastSelectedButton = selectedButtons[this.props.index];
const passwordString = passwordArray[selectedButtons[this.props.index]];
To get an index you have to create and pass one in, so create it when you map over the selected Buttons in the handleResults function in your Input component.
handleResults() {
return this.state.selectedButtons.map((el, index) => (
<Results key={el} appState={this.state} index={index} />
));
}

Categories