re-render not behaving properly - reactjs

So I deployed the app to demonstrate the problem
https://ecstatic-mayer-aaa530.netlify.app/
When you 'refresh' the page new data is fetched and user name is displayed (along with active state)
BUT when I click random user button, I don't get the same behavior even though I refetch the data. I want to get the same behavior as page refresh. (Name is displayed with the active state)
To better observe the problem you can : Hover on other fields, click on random user btn and then re-enter with your mouse on name (profile icon). It is not updating as you'll see
import './App.css';
import ProfileCard from './components/ProfileCard';
import React, { useEffect, useState } from 'react';
function App() {
const [userData, setUserData] = useState({});
const [triggerFetch, setTriggerFetch] = useState(false);
const fetchUserData = async () => {
const response = await fetch('https://randomuser.me/api/');
const data = await response.json();
setUserData(setFields(data.results[0]));
};
const setFields = (userData) => {
const fName = userData.name.first;
const lName = userData.name.last;
const fullName = fName + ' ' + lName;
return {
image: userData.picture.large,
name: fullName,
email: userData.email,
dob: userData.dob.date,
location: userData.location.street.name,
phone: userData.phone,
password: userData.login.password,
};
};
useEffect(() => {
fetchUserData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [triggerFetch]);
//this triggers fetch
const handleRandomUserClick = () => {
setTriggerFetch(!triggerFetch);
};
return (
<main>
<div className='grey-bg'></div>
<ProfileCard
userData={userData}
handleRandomUserClick={handleRandomUserClick}
/>
</main>
);
}
export default App;
import React from 'react';
import {
FaUserAlt,
FaPhoneAlt,
FaBirthdayCake,
FaLock,
FaLocationArrow,
FaEnvelope,
} from 'react-icons/fa';
const ProfileCard = ({ userData, handleRandomUserClick }) => {
const { image, name, email, dob, location, phone, password } = userData;
const [randomUserClicked, setrandomUserClicked] = React.useState(false);
const defaultFlag = {
name: 'active',
email: '',
dob: '',
location: '',
phone: '',
password: '',
};
const [flag, setFlag] = React.useState(defaultFlag);
const [title, setTitle] = React.useState('My Name Is');
const [value, setValue] = React.useState(name);
const handleValue = (e) => {
if (e.target.dataset.label === 'name') {
setTitle('My Name is');
setValue(name);
setFlag(defaultFlag);
} else if (e.target.dataset.label === 'email') {
setTitle('My Email is');
setValue(email);
setFlag({
name: '',
email: 'active',
dob: '',
location: '',
phone: '',
password: '',
});
} else if (e.target.dataset.label === 'dob') {
setTitle('My Birthday is');
setValue(new Date(dob).toLocaleDateString());
setFlag({
name: '',
email: '',
dob: 'active',
location: '',
phone: '',
password: '',
});
} else if (e.target.dataset.label === 'location') {
setTitle('I live in');
setValue(location);
setFlag({
name: '',
email: '',
dob: '',
location: 'active',
phone: '',
password: '',
});
} else if (e.target.dataset.label === 'phone') {
setTitle('My phone number is');
setValue(phone);
setFlag({
name: '',
email: '',
dob: '',
location: '',
phone: 'active',
password: '',
});
} else {
setTitle('My password');
setValue(password);
setFlag({
name: '',
email: '',
dob: '',
location: '',
phone: '',
password: 'active',
});
}
if (e.target.dataset.label === 'random') {
handleRandomUserClick();
setrandomUserClicked(!randomUserClicked);
}
};
React.useEffect(() => {
setTitle('My Name is');
setValue(name);
setFlag(defaultFlag);
}, [randomUserClicked]);
return (
<article className='profile-card'>
<img src={image} alt={name} />
<div className='user-details'>
<p className='user-title'>{title}</p>
<h3 className='user-value'>{value || name}</h3>
</div>
<ul className='card-list'>
<li className={flag.name} onMouseEnter={handleValue} data-label='name'>
<FaUserAlt className='icon' />
</li>
<li
data-label='email'
onMouseEnter={handleValue}
className={flag.email}
>
<FaEnvelope className='icon' />
</li>
<li data-label='dob' onMouseEnter={handleValue} className={flag.dob}>
<FaBirthdayCake className='icon' />
</li>
<li
data-label='location'
onMouseEnter={handleValue}
className={flag.location}
>
<FaLocationArrow className='icon' />
</li>
<li
data-label='phone'
onMouseEnter={handleValue}
className={flag.phone}
>
<FaPhoneAlt className='icon' />
</li>
<li
data-label='password'
onMouseEnter={handleValue}
className={flag.password}
>
<FaLock className='icon' />
</li>
</ul>
{/* Also handling this click within handleValue function */}
<button className='btn' onClick={handleValue} data-label='random'>
Random User
</button>
</article>
);
};
export default ProfileCard;

The problem happens, because in ProfileCard component, userData is not listed under useEffects's dependencies array.
By the way, your component design is very prone to bugs. In React functional components should be a simple functions that receive some data (props) and return JSX. Hooks should be used only when you actually have to use them. In your app, you're creating a complicated network of effects, state updates and re-renders, which makes it hard to maintain.
Let's write all hooks you actually need:
one state for the data fetched from an api
one state for keeping which section is currently active (name, email, etc.)
one effect for fetching the data
And that's it! Everything else can be just passed through props.

Related

DraftJS createWithContent returns undefined reading 'getBlockMap'

I am using draftjs to update data from a server call. I am using MongoDB as the database and graphql as the query language. I can convert the data from the API using convertFromRaw(JSON.parse(data)) . I then set it to state, but when I attempt to createWithContent() i get an error Cannot read properties of undefined (reading 'getBlockMap'). My code is below. Been working on this for two days.
const GET_ISP_ENTRY = gql`
query IspListEntry($ispListEntryId: ID!) {
ispListEntry(id: $ispListEntryId) {
_id
displayName
contactFirstName
contactLastName
contactTitle
lastUpdated
onlineService
onlineAttn
address
city
state
zipCode
country
phoneNumber
extension
mobileNumber
faxNumber
email
website
referTo
notes
previous
}
}
`;
const UPDATE_ISP_ENTRY = gql`
mutation UpdateISPEntry($ispListEntryUpdateId: ID!, $input: UpdateISPEntry) {
ispListEntryUpdate(id: $ispListEntryUpdateId, input: $input) {
displayName
}
}
`;
const UpdateISPEntry = () => {
const ispEntryFields = {
displayName: '',
lastUpdated: Date(),
onlineService: '',
contactTitle: '',
contactFirstName: '',
contactLastName: '',
onlineAttn: '',
address: '',
city: '',
state: '',
zipCode: '',
country: '',
phoneNumber: '',
extension: '',
mobileNumber: '',
faxNumber: '',
email: '',
website: '',
referTo: '',
notes: '',
previous: ''
};
const [rawNotesFromDB, setRawNotesFromDB] = useState();
const [urlId, setUrlId] = useState('');
const [getIsp, { data, loading, error }] = useLazyQuery(GET_ISP_ENTRY, {
variables: {
ispListEntryId: urlId
},
onCompleted: () => {
loading
? console.log('Loading....')
: setEditorNotesState(
convertFromRaw(JSON.parse(data.ispListEntry.notes))
);
},
onError: () => {
toast.error(error);
}
});
const [editorNotesState, setEditorNotesState] = useState(() =>
EditorState.createWithContent().getCurrentContent()
);
console.log(editorNotesState);
const [formValues, setFormValues] = useState();
const [previousValue, setPreviousValue] = useState();
const [editorPreviousState, setEditorPreviousState] = useState();
const [
submitValues,
{ data: successful, loading: successLoading, error: loadingError }
] = useMutation(UPDATE_ISP_ENTRY, {
onError: () => {
toast.error(`There was an error ${loadingError}`);
}
});
const params = useLocation();
const path = params.pathname;
const pathSplit = path.split('/')[2];
useEffect(() => {
getIsp();
setFormValues(data && data.ispListEntry);
setUrlId(pathSplit);
}, [data, getIsp, pathSplit, formValues]);
// const convertedState = convertFromRaw(
// JSON.parse(data && data.ispListEntry.notes)
// );
// console.log(convertedState);
// const raw = () => {
// !formValues
// ? console.log('DAMN')
// : setRawNotes(convertFromRaw(JSON.parse(formValues.notes)));
// };
const handleSubmit = () => {};
return (
<Fragment>
<div className='container p-4 parent-container'>
<ISPFormHeader />
<ISPFormHeaderPagename children='Update ISP Entry' />
<ISPForm
initialValues={data && data.ispListEntry}
enableReinitialize={true}
onSubmit={handleSubmit}
/>
<div className='editor-fields'>
<div className='rt-editor'>
<header className='rt-editor-header'>Notes</header>
<EditorComponent
id='notes'
name='notes'
type='text'
editorState={editorNotesState}
onEditorStateChange={setEditorNotesState}
/>
</div>
<div className='rt-editor'>
<header className='rt-editor-header'>Previous</header>
<EditorComponent
name='previous'
id='previous'
type='text'
EditorState={editorPreviousState}
// onEditorStateChange={handleEditorPreviousChange}
/>
</div>
</div>
</div>
</Fragment>
);
};
export default UpdateISPEntry;

How do I get a component to "listen" to changes in global state in a sibling component?

I'm building a wordle clone. I've structured it so that the keypad and the letter display grid are two separate components, Keypad.js and Row,js respectively. Project structure is as follows:
src
-components
|-Grid.js
|-Keypad.js
|-Row.js
-App.js
-AppContex.js
-index.js
When a user enters a letter on the keypad, initially I want that letter to appear in the first index of the first row, as per the game of wordle. How do I get Row.js to "listen" to changes in Keypad.js, so that when a user enters a letter, it shows up in the corresponding index in the grid row?
My approach so far has been to create global state using the Context API, where I've made an empty grid to share to the entire app:
AppContext.js
import { createContext } from "react";
const guessRows = [
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', '']
]
export const AppContext = createContext()
const AppContextProvider = (props) => {
return(
<AppContext.Provider value = {guessRows}>
{props.children}
</AppContext.Provider>
)
}
export default AppContextProvider
In Keypad.js, the letter the user enters is used to update the context (or at least that's what I think it's doing):
import { useContext,useState } from "react"
import { AppContext } from "../AppContext"
const Keypad = () => {
const guessRows = useContext(AppContext);
let currentRow = 0;
let currentTile = 0;
const letters = [
"Q",
"W",
"E",
"R",
// etc
];
const handleClick = (letter) => {
guessRows[currentRow][currentTile] = letter;
currentTile++;
console.log("guess rows", guessRows);
};
return (
<div className="keyboard-container">
{letters.map((letter, index) => {
return (
<div className="key" key={index} onClick={() => handleClick(letter)}>
{letter}
</div>
);
})}
</div>
);
};
export default Keypad;
...then in Row.js, I'm looping through the context and rendering the rows:
Row.js
import { useContext } from "react";
import { AppContext } from "../AppContext";
const Row = () => {
const rowData = useContext(AppContext)
const currentRow = rowData[0]
return (
<div className="row">
{currentRow.map((letter,index) => {
return(
<div className="tile" id = {index}>{letter}</div>
)
})}
</div>
)
}
export default Row;
Unsurprisingly this isn't working, so any suggestions would be appreciated.
Your guessRows should be put into the context state, so that it can have a state setter passed down - then instead of doing guessRows[currentRow][currentTile] = letter;, call the state setter. Similarly, currentTile++; should be replaced with a state update, since this is React - the view should flow from the state.
const AppContextProvider = (props) => {
const [guessRows, setGuessRows] = useState([
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', ''],
['', '', '', '', '']
]);
return(
<AppContext.Provider value = {{ guessRows, setGuessRows }}>
{props.children}
</AppContext.Provider>
);
};
const { guessRows, setGuessRows } = useContext(AppContext);
const [currentRow, setCurrentRow] = useState(0);
const [currentTile, setCurrentTile] = useState(0);
const handleClick = (letter) => {
setGuessRows(
guessRows.map((row, i) => i !== currentRow ? row : (
row.map((item, j) => j === currentTile ? letter : item)
))
);
setCurrentTile(currentTile + 1);
};
And then when the state setter is called, the components will re-render, including Row, which will show the changes made.

Form inputs should clear only with correct submit

My form component looks like :
constructor(props) {
super(props);
this.state = {
startDate: '',
endDate: '',
description: '',
profileId: this.props.profileId
}
this.onStartDateChange = this.onStartDateChange.bind(this);
this.onEndDateChange = this.onEndDateChange.bind(this);
this.onDescriptionChange = this.onDescriptionChange.bind(this);
this.clickHandle = this.clickHandle.bind(this);
}
onStartDateChange(event) {
const startDate = event.target.value
this.setState({
startDate: startDate
})
}
onEndDateChange(event) {
const endDate = event.target.value
this.setState({
endDate: endDate
})
}
onDescriptionChange(event) {
const description = event.target.value
this.setState({
description: description
})
}
clickHandle = () => {
const inputValues = {
startDate: this.state.startDate,
endDate: this.state.endDate,
description: this.state.description
}
this.props.onAddClick(this.state.profileId, inputValues);
}
render() {
return (
<Form>
<div className={"m-3 form-row"}>
<Field type={"date"} className={"form-control col-md-2 mr-2"}
onChange={evt => this.onStartDateChange(evt)}
value={this.state.startDate}
name={"add-start-date"}/>
<Field type={"date"} className={"form-control col-md-2 mr-2"}
onChange={evt => this.onEndDateChange(evt)}
value={this.state.endDate}
name={"add-end-date"}/>
<Field className={"form-control col-md-2 mr-2"}
type={"textarea"}
onChange={evt => this.onDescriptionChange(evt)}
value={this.state.description}
name={"add-description"}/>
<button className="btn btn-info round-btn" href="#"
onClick={this.clickHandle}
type={"button"}><FontAwesomeIcon icon={faPlus}/></button>
</div>
</Form>
}
My action looks like:
export const onAddClick = (profileId, educationData = {
startDate: '',
endDate: '',
description: ''
}) => {
return (dispatch) => {
const education = {
startDate: educationData.startDate,
endDate: educationData.endDate,
description: educationData.description
};
return axios.post(`${ENTRY_API_URL}`)
.then(response => {
dispatch(_addEducation(profileId, response.data))
})
.catch((error) => {
if (error) {
dispatch(_requestFailure(profileId, error.response.data))
}
})
};
};
My store looks like
workExperience: [
{
id: '5d6e3368993694389c903c50',
startDate: '2019-09-04',
endDate: '2019-09-12',
description: 'description'
},
How can I clear input only when request to API is correct and when its wrong leave it. Can I make it without redux-forms ? I tried with some flag in store but it change slowly after I clicked the button. Should I try make some state in redux for this form ?

How to clear input text box, after obtaining the result from API in React

I need help with my React weather app. I am searching for a city to obtain a weather forecast from API. Everything is working. When I enter the city name and search, result is appearing. But I want to clear the input text when the result is obtained. I did the following resetForm, It is clearing the input field but at the same time clearing the result from API.
class Form extends React.Component{
render(){
resetForm = () => {
this.refs.inputId.value="";
}
return(
<form onSubmit={this.props.getWeather}>
<input type="text" name="city" placeholder="Type a city name..." ref="inputID" />
<button>Search</button>
</form>
);
}
};
getWeather = async (e) =>{
this.setState({loading: true});
e.preventDefault();
const city = e.target.elements.city.value;
const api_call = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric`);
const data = await api_call.json();
if(city){
console.log(data);
this.setState({
temperature: data.main.temp,
city: data.name,
humidity: data.main.humidity,
description: data.weather[0].description,
error: "",
loading:false,
});
}else{
this.setState({
temperature: undefined,
city: undefined,
humidity: undefined,
description: undefined,
error: "Please enter the city and the country name!",
loading:false,
});
}
Controlled Components is the simplest way to do this.
In parent component, you need to maintain a state variable and change handler function and pass it to the Form component,
state = {
inputCity: '' //You can add this in your existing state
}
And the change handler function,
onCityChange = (e) => {
this.setState({inputCity : e.target.value})
}
Now you caan pass them to your Form component,
<Form inputCity={this.state.inputCity} onCityChange={this.onCityChange} getWeather={this.getWeather}/>
You Form component should be,
class Form extends React.Component{
render(){
return(
<form onSubmit={this.props.getWeather}>
<input type="text" name="city" placeholder="Type a city name..." value={this.props.inputCity} onChange={this.props.onCityChange}/>
<button>Search</button>
</form>
);
}
};
Your getWeather function should be,
getWeather = async (e) =>{
this.setState({loading: true});
e.preventDefault();
const city = this.state.inputCity; //take the city from state directly
const api_call = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric`);
const data = await api_call.json();
if(data){ //instead of city it should be data here
console.log(data);
this.setState({
temperature: data.main.temp,
city: data.name,
humidity: data.main.humidity,
description: data.weather[0].description,
error: "",
loading:false,
inputCity: "" //here you can clear the city
});
}else{
this.setState({
temperature: undefined,
city: undefined,
humidity: undefined,
description: undefined,
error: "Please enter the city and the country name!",
loading:false,
});
}
Try this: I have modified your code a little to use state, ref is fine but is usually considered not the most efficient approach:
this.setState has a callback as well since it's an async operation, you can make your API request after loading is set to true, after the response is successful or fails (as soon as the response from the server is recieved), set the loading state to false and clear the input.
The rest of your code is fine, except for a few tweaks, it will now work.
Read more about controlled inputs here: https://reactjs.org/docs/forms.html
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class Form extends React.Component{
state = {
loading:false,
temperature: '',
city: '',
humidity: '',
description: '',
error: '',
cityName: ''
}
handleChange = (e) => {
this.setState({ city: e.target.value });
}
getWeather = (e) => {
e.preventDefault();
this.setState({loading: true}, async () => {
const {city} = this.state;
const api_call = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric`);
const data = await api_call.json();
if(data){
console.log(data);
this.setState({
temperature: data.main.temp,
cityName: data.name,
humidity: data.main.humidity,
description: data.weather[0].description,
error: "",
loading:false,
}, () => {
this.setState({ city: '' })
});
}else{
this.setState({
temperature: undefined,
cityName: undefined,
humidity: undefined,
description: undefined,
error: "Please enter the city and the country name!",
loading:false,
}, () => {
this.setState({ city: '' })
});
}
});
}
render(){
return(
<form onSubmit={this.getWeather}>
<input type="text"
name="city"
value={this.state.city}
placeholder="Type a city name..."
onChange={this.handleChange} />
<button>Search</button>
</form>
);
}
};
const rootElement = document.getElementById("root");
ReactDOM.render(<Form />, rootElement);
Sandbox link: https://codesandbox.io/s/zen-browser-e2rh8?fontsize=14

Want to populate the input values based on the click of an object inside map(): React+Typescript

I am maintaining an array of objects which is stored in a state object. Basically I am pushing each object to this array whenever I click on Add button .This stores this object in array.
Also I am iterating this array of objects to display down the page.
Right now I am trying to fill the input fields based on the object that I have clicked. I am unable to do it. Basically, the object that I have clicked should populate the input fields and then I should be able to edit it
Help would be appreciated
The structure of array of objects:
users= [
{"name":"xxx","email":"yyy","phone":"656"},
{"name":"yyy","email":"xxx","phone":"55"}
];
Component Code
import * as React from 'react';
interface IState{
users : Account[];
user: Account
}
interface Account{
name: string;
email: string;
phone: string
}
export default class App extends React.Component<{},IState> {
constructor(props:any){
super(props);
this.state= {
users: [],
user: {
name: '',
email: '',
phone: '',
}
}
}
removeAccount = (i:number) => {
let users = [...this.state.users];
users.splice(i,1);
this.setState({users},()=>{console.log('setting the data')});
}
handleChange = ( event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
user:{
...this.state.user,
[event.currentTarget.name]:event.currentTarget.value
}
})
}
onAdd = () => {
e.preventDefault();
this.setState({
users: [...this.state.users, this.state.user],
user: { name:'', email: '', phone: ''}
},()=>{console.log('adding')});
}
clearInputs = () => {
this.setState({user: { name:'', email: '', phone: ''}});
}
showDetails = (i:number) => { //I need to populate the input fields based on the index of the object clicked.
console.log(i);
}
render(){
const { name, email, phone } = this.state.user;
<React.Fragment>
<form onSubmit={this.onAdd}>
<input type="text" value={name} onChange={(e:any) => this.handleChange(e)} name={"name"} />
<input type="text" value={email} onChange={(e:any) => this.handleChange(e)} name={"email"} />
<input type="text" value={phone} onChange={(e:any) => this.handleChange(e)} name={"phone"} />
<button type="submit">Add</button>
</form>
<ul>
{this.state.users.map((row:any ,index: number) =>
<li key={index}>
<a onClick={()=> this.showDetails(index)}><span>{row.name}</span></a> // on click of this,i need to display the values corresponding to this object in the above input fields
<i className="close far fa-times" onClick={() =>this.removeAccount(index)}/>
</li>
)}
</ul>
</React.Fragment>
}
}
Based on logic of the code showDetails should look like
showDetails = (i:number) => {
this.setState ({user: this.state.users.splice(i,1)});
console.log(i);
}
Just set user to the selected element of users array. React will do update and calls render() with updated data.
Also utilizing splice will remove currently editing user from array. THis follow logic of the code. After edit Add should be clicked to add modified user back to array. This may be not convenient, so you may consider adding editingIndex to state and specify which user object currently editing. In such case you'll have to save index of selected object in editingIndex. In handleChange you should check if some user object editing now and modify data not only in user property of state but in corresponding users array element
interface IState{
users : Account[];
user: Account;
editingIndex: number | null;
}
// In constructor
constructor(props:any){
super(props);
this.state= {
users: [],
user: {
name: '',
email: '',
phone: '',
},
editingIndex: null
}
}
showDetails = (i:number) => {
this.setState ({user: this.state.users[i], editingIndex: i});
console.log(i);
}
handleChange = ( event: React.ChangeEvent<HTMLInputElement>) => {
let user = {...this.state.user,
[event.currentTarget.name]:event.currentTarget.value};
this.setState({user});
// If we currently editing existing item, update it in array
if (this.state.editingIndex !== null) {
let users = [...this.state.users];
users[this.state.editingIndex] = user;
this.setState({users});
}
}
removeAccount = (i:number) => {
let users = [...this.state.users];
// If we're going to delete existing item which we've been editing, set editingIndex to null, to specify that editing ends
if (this.state.editingIndex === i)
this.setState({user: {name: '', email: '', phone: ''}, editingIndex: null});
users.splice(i,1);
this.setState({users},()=>{console.log('setting the data')});
}
onAdd = () => {
e.preventDefault();
// If we NOT editing, but adding new editingIndex will be null so add user to users array. If we editing existing element it's no need to add it once again.
if (this.state.editingIndex === null)
this.setState({ users: [...this.state.users, this.state.user] });
this.setState ({ editingIndex: null,
user: { name:'', email: '', phone: ''}
},()=>{console.log('adding')});
}
// render will have no change

Resources