Rendered more hooks than during the previous render. when posting form data with React Hooks - reactjs

Ran into a problem with hooks today. I know there is a similar post, and I read the rules of using hooks. Right now when I post my form, it gives me that error. And I know that's because my hook is INSIDE an if statement. But how can I get it out? I don't know how else to use this hook if it's not in a function or a statement. Any advice would be greatly appreciated. Here is the code:
import React, { FunctionComponent, useState, useEffect } from 'react';
import usePost from '../hooks/usepost'
import Article from './article';
interface ArticlePosted {
title: string,
body: string,
author: string
}
const Post: FunctionComponent = () => {
const [details, detailsReady] = useState({})
const postArticle = (e) => {
e.preventDefault()
const postDetails = {
title: e.target.title.value,
body: e.target.body.value,
author: e.target.author.value
}
detailsReady(postDetails)
}
if (Object.keys(details).length !== 0) {
console.log(details)
usePost('http://localhost:4000/kb/add', details)
}
return (
<div>
<form onSubmit={postArticle}>
<p>
Title <input type='text' name='title' />
</p>
<p>
Body <textarea name='body' rows={4} />
</p>
<p>
Author <input type='text' name='author' />
</p>
<button type='submit'>Submit Article</button>
</form>
</div>
);
};
export default Post;
Custom Hook:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const usePost = (url, postDetails) => {
//set empty object as data
console.log(url, "DFLSKDJFSDLKFJDLKJFDLFJ")
console.log(postDetails)
useEffect(() => {
console.log('usePost running')
axios.post(url, postDetails)
.then(res => {
console.log(res)
return
})
}
, [postDetails]);
};
export default usePost

You can move the if-statement logic into the usePost hook.
const usePost = (url, postDetails) => {
useEffect(() => {
if (Object.keys(postDetails).length === 0){
return console.log('Not posting'); // Don't post anything if no details
}
// Otherwise, post away
console.log('usePost running')
axios.post(url, postDetails)
.then(res => {
console.log(res)
return
})
}
, [postDetails]);
};

Related

How to update state value to textarea using onChange?

Currently following a slightly older tutorial, but learning using React 18 -- trying to update the text area in a notes app
It looks like when I type, a character appears and then immediately is deleted automatically
Can anyone confirm if I might be missing a detail here?
for reference if familiar with the project at time 1:37:03 : https://www.youtube.com/watch?v=6fM3ueN9nYM&t=377s
import React, {useState, useEffect} from 'react'
import notes from '../assets/data'
import { useParams } from 'react-router-dom';
import { Link } from 'react-router-dom'
import { ReactComponent as ArrowLeft } from '../assets/arrow-left.svg'
const NotePage = ( history ) => {
const {id} = useParams();
// let note = notes.find(note => note.id===Number(id))
// console.log(id)
let [note, setNote] = useState(null)
useEffect(() => {
getNote()
}, [{id}])
let getNote = async () => {
let response = await fetch(`http://localhost:8000/notes/${id}`)
let data = await response.json()
setNote(data)
}
// let updateNote = async () => {
// await fetch(`http://localhost:8000/notes/${id}`, {
// method: 'PUT',
// headers: {
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify({...note, 'updated':new Date()})
// })
// }
// let handleSubmit = () => {
// updateNote()
// history.push('/')
// }
return (
<div className="note">
<div className="note-header">
<h3>
<Link to="/">
<ArrowLeft /*onClick={handleSubmit}*/ />
</Link>
</h3>
</div>
<textarea onChange={(e) => {
setNote({...note, 'body': e.target.value}) }}
value={note?.body}>
</textarea>
</div>
)
}
export default NotePage
Your value in the useEffect dependency array is incorrect and causing getNote to be called every time you make changes in the textArea. Every time getNote is called, it's resetting the note state back to whataver is being received by getNote. Which in your case is probably a blank note
Change this :
useEffect(() => {
getNote();
}, [{ id }]);
To this:
useEffect(() => {
getNote();
}, [id]);

Why doesn't the axios response get saved in useState variable

I've built a random photo displaying feature in react.
the console says that the response is valid and it works,
but the page breaks when I return data.
Where is the issue?
Thanks in advance!
import React from 'react'
import { useEffect, useState } from 'react'
import axios from 'axios'
function RandomPhoto() {
const url = `https://api.unsplash.com/photos/random/?client_id=${process.env.REACT_APP_UNSPLASH_KEY}`
const [data, setData] = useState()
const getPhoto = () => {
axios.get(url)
.then(response => {
setData(response.data)
console.log(response.data) // <------- works
})
.catch(error => {
console.log(error)
})
}
useEffect(() => {
getPhoto()
},[])
console.log("XX" + data) // <---------- doesn't work, and following return() neither
return (
<div>
<img href={data.urls.regular} alt={data.alt_description}/>
<p>Photo by {data.username} {data.name} from {data.location} - found on unsplash</p>
</div>
)
}
export default RandomPhoto
I modified your code a bit, and it's working. I made it as an async function and changed the path of JSON object keys.
Please note the location data sometimes returns as null. So you have to render it conditionally.
import React from 'react';
import { useEffect, useState } from 'react';
import axios from 'axios';
const RandomPhoto = () => {
const url = `https://api.unsplash.com/photos/random/?client_id=${process.env.REACT_APP_UNSPLASH_KEY}`;
const [imageData, setImageData] = useState('');
const getPhoto = async () => {
await axios
.get(url)
.then((response) => {
setImageData(response.data);
})
.catch((error) => {
console.log(error);
});
};
useEffect(() => {
getPhoto();
}, []);
return (
<div>
<p>Hello</p>
<img src={imageData.urls?.regular} />
<p>
Photo by {imageData?.user?.username} {imageData?.user?.name} from{' '}
{imageData?.location?.country} - found on unsplash
</p>
</div>
);
};
export default RandomPhoto;

How to pass data from child component to parent component?

I am currently learning React, and I am trying to build a small weatherapp to practice with apis, axios and react generally. I built an input component where it's duty is getting the data from the API, and I am holding the data in the useState hook and I want to use the data in the main App component? I am able to pass data from parent App component to input component if I take the functionality in the app component, but this time I start to have problems with input text rendering problems. Here is the code:
this is the input component where I search and get the data from the API, and I am trying to pass the weatherData into the main App component and render it there. How is it possible to achieve this?
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const key = process.env.REACT_APP_API_KEY;
function SearchLocation() {
const [text, textChange] = useState('');
const [weatherData, setWeatherData] = useState([]);
const handleText = (e) => {
textChange(e.target.value);
};
const fetchData = async () => {
const { data } = await axios.get(
`https://api.weatherapi.com/v1/current.json`,
{
params: {
key: key,
q: text,
lang: 'en',
},
}
);
setWeatherData(data);
};
useEffect(() => {
try {
fetchData();
} catch (error) {
console.log(error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [text]);
console.log(weatherData);
return (
<div>
<form>
<input
onChange={handleText}
className="locationInput"
type="text"
value={text}
required
></input>
</form>
</div>
);
}
export default SearchLocation;
EDIT:
After moving the states to main component and passing them to children as props I receive 3 errors, GET 400 error from the API, createError.js:16 Uncaught (in promise) Error: Request failed with status code 400 and textChange is not a function error. Here are how components look like. This is the input component:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const key = process.env.REACT_APP_API_KEY;
function SearchLocation({ weatherData, setWeatherData, text, textChange }) {
const handleText = (e) => {
textChange(e.target.value);
};
const fetchData = async () => {
const { data } = await axios.get(
`https://api.weatherapi.com/v1/current.json`,
{
params: {
key: key,
q: text,
lang: 'en',
},
}
);
setWeatherData(data);
};
useEffect(() => {
try {
fetchData();
} catch (error) {
console.log(error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [text]);
console.log(weatherData);
return (
<div>
<form>
<input
onChange={handleText}
className="locationInput"
type="text"
value={text}
required
></input>
</form>
</div>
);
}
export default SearchLocation;
this is the parent app component:
import React from 'react';
import { useState } from 'react';
import './App.css';
import './index.css';
import SearchLocation from './components/Input';
function App() {
const [weatherData, setWeatherData] = useState([]);
const [text, textChange] = useState('');
return (
<div className="App">
<SearchLocation
setWeatherData={setWeatherData}
lastData={weatherData}
inputText={text}
/>
</div>
);
}
export default App;
You'll still need to store the state in the parent component. Pass the setter down as a prop. This is a React pattern called Lifting State Up.
Example:
const App = () => {
const [weatherData, setWeatherData] = useState([]);
...
return (
...
<SearchLocation setWeatherData={setWeatherData} />
...
);
};
...
function SearchLocation({ setWeatherData }) {
const [text, textChange] = useState('');
const handleText = (e) => {
textChange(e.target.value);
};
const fetchData = async () => {
const { data } = await axios.get(
"https://api.weatherapi.com/v1/current.json",
{
params: {
key,
q: text,
lang: 'en',
},
}
);
setWeatherData(data);
};
useEffect(() => {
try {
// Only request weather data if `text` is truthy
if (text) {
fetchData();
}
} catch (error) {
console.log(error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [text]);
return (
<div>
<form>
<input
onChange={handleText}
className="locationInput"
type="text"
value={text}
required
/>
</form>
</div>
);
}
There are two solutions to your problem:-
Firstly you can create the states const [text, textChange] = useState('');
const [weatherData, setWeatherData] = useState([]);, inside your parent component and pass text, textChange, weatherData, setWeatherData as props to your child component.
I would recommend the second way, i.e, implement redux for this and store text, and weatherData into your redux and try to access them from redux.
redux reference:- https://react-redux.js.org/introduction/getting-started

React-Redux Update Form (PUT request) issue

I am trying to update a form but something is not working as it should. After I click Update, the updated information is logged in the console, but it seems that the Redux side of the state management is not working. I am not getting any errors in the console, but neither my action UPDATE_POST is visible in Redux Dev Tools on Chrome.
Here is the code:
The UpdateForm component:
import { useState , useEffect} from "react";
import { useHistory, useParams } from 'react-router-dom';
import jsonPlaceholder from "../apis/jsonPlaceholder";
import {updatePost} from '../actions'
import { useDispatch } from 'react-redux';
const UpdateForm = () => {
const dispatch = useDispatch()
const history = useHistory();
const { id } = useParams();
const [post, setPost] = useState({});
const [title, setTitle] = useState(post.title);
const [body, setBody] = useState(post.body);
const [author, setAuthor] = useState(post.author);
const fetchPost = async () => {
const response = await jsonPlaceholder.get(`/posts/${id}`)
console.log(response.data)
setPost(response.data)
setTitle(response.data.title)
setBody(response.data.body)
setAuthor(response.data.author)
return response.data
}
useEffect(() => {
fetchPost();
}, [])
const handleUpdate = async (e) => {
e.preventDefault();
const post = { title, body, author }
dispatch(updatePost(post))
console.log('post', post)//updated post is logged in console
history.push('/')
}
console.log("title", title)
return (
<div className="create">
<h2>Update Blog</h2>
<form>
<label>Blog title:</label>
<input
type="text"
required
defaultValue={title}
onChange={(e) => setTitle(e.target.value)}
/>
<label>Blog body:</label>
<textarea
required
defaultValue={body}
onChange={(e) => setBody(e.target.value)}
></textarea>
<label>Author:</label>
<input
type="text"
required
defaultValue={author}
onChange={(e) => setAuthor(e.target.value)}
/>
<button onClick={handleUpdate}>Update</button>
</form>
</div>
);
}
export default UpdateForm;
The action:
export const updatePost = (post) => async dispatch => {
const res = await jsonPlaceholder.put(`posts/update/${post._id}`);
dispatch({
type: UPDATE_POST,
payload: res.data
})
}
And the reducer:
import { ADD_POST, DELETE_POST, UPDATE_POST } from '../actions/types';
const postReducer = (state = [], action) => {
switch (action.type) {
case ADD_POST:
return state.concat([action.data]);
case UPDATE_POST:
return {
...state,
post: action.data
}
case DELETE_POST:
return state.filter((post)=>post.id !== action.id);
default:
return state
}
}
export default postReducer;
Here is the node.js/express server side of the request:
router.put('/update/:id', async (req, res) => {
try {
let post = await Post.findOneAndUpdate(req.params.id, {
title: req.body.title,
body: req.body.body,
author: req.author.body
})
console.log('server', post)
return res.json(post)
} catch (error) {
console.error(error.message);
res.status(500).send('Server Error')
}
})
I am now getting server error (500), and if I remove the line author: req.author.body, I am not getting the error. The code on the front still does not work.
As I see you are directly calling your actions instead of dispatching it
import useDispatch and use it like this
import { useDispatch } from "react-redux";
UpdateForm.js
const UpdateForm = () => {
....
const dispatch = useDispatch();
.....
const handleUpdate = async (e) => {
e.preventDefault();
const post = { title, body, author }
dispatch(updatePost(post)) // dispatch like this
console.log('post', post)//updated post is logged in console
history.push('/')
}
console.log("title", title)
return (
<div className="create">
.......
</div>
);
}
export default UpdateForm;
reducer
instead of action.payload, you're accessing action.data
case UPDATE_POST:
return {
...state,
post: action.payload
}
You need to dispatch the updatePost action, not call it directly. You're missing useDispatch call.
Here's a link to React Redux documentation covering it:
https://react-redux.js.org/api/hooks#usedispatch
Example:
import React from 'react'
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
return (
<div>
<span>{value}</span>
<button onClick={() => dispatch({ type: 'increment-counter' })}>
Increment counter
</button>
</div>
)
}
[UPDATE]
Just noticed that your updatePost action is an higher order function so once you add the call to useDispatch you'll need to change the call to updatePost from
updatePost(post)
to
updatePost(post)(dispatch)
To be honest I would probably go with a book action creator and move the API call to the component itself. If you're interested in async actions I would suggest looking into react-thunk, it is fairly easy to begin with.
[UPDATE 2]
There seem to be a typo in the express code.
req.author.body
should be
req.body.author
[UPDATE 3]
The post object in the updatePost does not contain the _id field (check your handleUpdate function) thus you're getting the url: "posts/update/undefined".

Activating 2 different calls on react onClick

I am facing the next scenario on my react component:
I have 2 buttons, which do pretty much the same except the fact that they fetch the data from different places.
The data is fetched from 2 different function on axios service class, but when returning from server, the same code should be executed (on then and catch methods).
How can I make that only the fetching will be a separate call, but the rest of the code will be the same?
axios file:
import axios from 'axios';
const ACCOUNT_API_BASE_URL = "http://localhost:8080/app";
class MyAxiosService {
getAccount1(accountNumber) {
return axios.post(ACCOUNT_API_BASE_URL + 'accounts', accountNumber);
}
getAccount2(accountNumber) {
return axios.post(ACCOUNT_API_BASE_URL + 'findSecondWayAccount', accountNumber);
}
}
export default new MyAxiosService()
Component:
import AccountService from '../services/AccountService'
import './web.css'
import Dropdown from 'react-dropdown'
import 'react-dropdown/style.css'
import React, { Component, useState } from 'react'
import { useSelector, useDispatch } from 'react-redux';
import { useAlert } from "react-alert";
import MyAxiosService from '../services/MyAxiosService';
const Example = (props) => {
const onClick1 = (event) => {
event.preventDefault();
MyAxiosService.getAccount1(1111).then(res => {
//common logic for both calls
}).catch(error => {
//common error handling for both calls
});
};
const onClick2 = (event) => {
event.preventDefault();
MyAxiosService.getAccount2(1111).then(res => {
//common logic for both calls
}).catch(error => {
//common error handling for both calls
});
};
return (
<form >
<div>
<div class="float">
<input id='accountNumber' pattern="\d{4}" maxlength="8" name='accountNumber' for="accountNumber" required='required' placeholder="מספר חשבון" value={findAccountCriteria.accountNumber} onChange={onInputChange} />
</div>
<div class="float">
<input type="submit" value="find1" class="ui-button" id="a1" onClick={onClick1} />
</div>
<div class="float">
<input type="submit" value="find2" class="ui-button" id="a2" onClick={onClick2} />
</div>
</div>
<br />
</form>
);
};
export default Example
You could do this.
const onClickCommon = (event, accountType) => {
event.preventDefault();
MyAxiosService[accountType](1111).then(res => {
//common logic for both calls
}).catch(error => {
//common error handling for both calls
});
};
<Button onClick={e => this.onClickCommon(e, "getAccount1")}>
I'm kind of new to React, but have you tried something like this:
axios file:
import axios from 'axios';
const ACCOUNT_API_BASE_URL = "http://localhost:8080/app";
class MyAxiosService {
getAccount(accountType, accountNumber) {
return axios.post(ACCOUNT_API_BASE_URL + accountType, accountNumber);
}
}
export default new MyAxiosService()
and the refactored part on component
const handleClick = useCallback((accountType, accountNumber) => {
MyAxiosService.getAccount(accountType, accountNumber).then(res => {
//common logic for both calls
}).catch(error => {
//common error handling for both calls
});
}, []);
const onClick1 = (event) => {
event.preventDefault();
handleClick('accounts', 1111);
};
const onClick2 = (event) => {
event.preventDefault();
handleClick('findSecondWayAccount', 1111);
};
Hope this works for you, or at all, since I've not tested. :)

Resources