React Redux not re-rendering, but state being copied? - reactjs

I've read the docs here but I am having trouble getting the component to rerender after state is updated. The posts are being added, I just have to rerender the component manually to get them to show up, what am I missing?
I have this in the component:
class ListPosts extends Component {
state = {
open: false,
body: '',
id: ''
}
openPostModal = () => this.setState(() => ({
open: true,
}))
closePostModal = () => this.setState(() => ({
open: false,
}))
componentWillMount() {
const selectedCategory = this.props.selectedCategory;
this.props.fetchPosts(selectedCategory);
}
handleChange = (e, value) => {
e.preventDefault();
// console.log('handlechange!', e.target.value)
this.setState({ body: e.target.value });
};
submit = (e) => {
// e.preventDefault();
console.log(this.state.body)
const body = this.state.body;
const id = getUUID()
const category = this.props.selectedCategory;
const post = {
id,
body,
category
}
this.props.dispatch(addPost(post))
this.closePostModal()
}
Then down below I am adding the dispatch to props...
const mapStateToProps = state => ({
posts: state.postsReducer.posts,
loading: state.postsReducer.loading,
error: state.postsReducer.error,
selectedCategory: state.categoriesReducer.selectedCategory,
// selectedPost: state.postsReducer.selectedPost,
});
function mapDispatchToProps (dispatch) {
return {
fetchPosts: (selectedCategory) => dispatch(fetchPosts(selectedCategory)),
addPost: (postObj) => dispatch(addPost(postObj)),
}
}
export default withRouter(connect(
mapStateToProps,
mapDispatchToProps
)(ListPosts))
Here is the code for the reducer:
case C.ADD_POST :
const hasPost = state.some(post => post.id === action.payload.postObj.id)
console.log('caseADD_POST:', action.payload.postObj.id)
return (hasPost) ?
state :
[
...state,
post(null, action)
];

Related

React redux update input field with user text after data filled from API

I am learning react hence this could be primitive problem but after lot of effort and searching I could not get this to work. Any help really appreciated.
Requirement
I have a html form which is filed by API response after user clicking a button. After API response user should be able to change the filled data.
Problem
From redux i set the properties from api call. This information set to state which uses to render to input field. when user types the data in input filed i cannot seems to change the state without causing infinite loop or not updating state at all.
This is my current work
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
title: 'Initial title'
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.props.hasOwnProperty('todoInfo')) {
const {title} = this.props.todoInfo;
if (title !== prevState.title) {
this.setState({
title: title
});
}
}
}
handleChange = (event) => {
const {name, value} = event.target;
this.setState((prevState) => ({
...prevState,
title: value
}));
}
render() {
const {title} = this.state;
return (
<div>
<p>
<button onClick={() => this.props.fetchData()}> fetch data</button>
</p>
<p>
<input type="text" id="fname" name="fname" value={this.state.title}
onChange={this.handleChange}></input>
</p>
</div>
);
}
}
const mapStateToProps = (state) => {
console.log('map state to props ' + JSON.stringify(state));
return {
todoInfo: state.todoReducers.todoInfo
};
};
const mapDispatchToProps = (dispatch) => ({
fetchData: () =>
dispatch(fetchData())
});
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
actions
export const fetchData = () => {
return (dispatch) => {
axios.get('https://jsonplaceholder.typicode.com/todos/1')
.then(response => {
const todoInfo = response.data;
dispatch({
type: GET_TODO,
payload: todoInfo
})
})
.catch(error => {
const errorMsg = error.message;
console.log(errorMsg);
});
};
};
reducer
const todoReducers = (state = {}, action) =>{
switch (action.type) {
case GET_TODO:
console.log('here ' + JSON.stringify(action));
return {
...state,
todoInfo: action.payload
};
default:
return state;
}
}
export default todoReducers;
I want my input text field load data from API then update with any user input. If they click fetch data again then user inputs are replaced by API fetch data. how to do this ?
EDIT
Solution according to accepted answer can be found https://github.com/mayuraviraj/react-redux-my-test
If your plan is to use redux, then you are trying to use a centralised state, don't mix it with the local state. (although it depends on your use case really)
Instead of your current code, remove the componentDidUpdate
class Counter extends Component {
constructor(props) {
super(props);
}
handleChange = (event) => {
const {name, value} = event.target;
this.props.changeTitle(value);
}
render() {
const {title} = this.state;
return (
<div>
<p>
<button onClick={() => this.props.fetchData()}> fetch data</button>
</p>
<p>
<input type="text" id="fname" name="fname" value={this.state.todoInfo}
onChange={this.handleChange}></input>
</p>
</div>
);
}
}
const mapStateToProps = (state) => {
console.log('map state to props ' + JSON.stringify(state));
return {
todoInfo: state.todoReducers.todoInfo
};
};
const mapDispatchToProps = (dispatch) => ({
fetchData: () =>
dispatch(fetchData()),
changeTitle: () => dispatch(changeTitle())
});
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
Create another action:
export const changeTitle = (value) => ({
type: CHANGE_TODO_INFO,
payload: value
});
};
Then handle it in your reducer:
const todoReducers = (state = {}, action) =>{
switch (action.type) {
case GET_TODO:
console.log('here ' + JSON.stringify(action));
return {
...state,
todoInfo: action.payload
};
case CHANGE_TODO_INFO:
return {
...state,
todoInfo: action.payload
};
default:
return state;
}
}
export default todoReducers;
Don't forget to create the CHANGE_TODO_INFO variable, you get the idea
update title from redux in handleChange.
This fix should help:
handleChange = (event) => {
const {name, value} = event.target;
this.setState((prevState) => ({
...prevState,
title: value
}));
changeTitle(value);
}
const mapDispatchToProps = (dispatch) => ({
fetchData: () => dispatch(fetchData()),
changeTitle: (value) => dispatch({ type: GET_TODO, payload: value }),
});

Create a react component that fetches data

I'd like to create a component that takes in an ID of a movie and from that ID I would be able to fetch data i would be able to get details from that movie. For example, in my moviecard component, I run this axios get to obtain data on a movie.
const Moviecard = (props) => {
const {movie} = props
const [details, setDetails] = useState('')
useEffect(()=> {
if(movie) {
axios.get(`https://api.themoviedb.org/3/movie/${movie.id}?api_key=mykey&append_to_response=videos,images`).then(resp=> {
setDetails(resp.data)
})
.catch(err=> {
console.log(err)
})
}
}, [movie])
return (
<div>Movie Card</div>
)
}
export default Moviecard
So, I would like to create a component that only handles getting the data, and then importing that state which would contain all the data
Think this is where you would make your own custom hook, you can do something like
//useFetchMovie.js
export const useFetchMovie = (movieId) => {
const [movieData, setMovieData] = useState({
isLoading: true,
error: false,
data: null,
});
useEffect(() => {
if (movieId) {
axios
.get(
`https://api.themoviedb.org/3/movie/${movie.id}?api_key=mykey&append_to_response=videos,images`
)
.then((resp) => {
setMovieData((oldMovieData) => ({
...oldMovieData,
isLoading: false,
data: resp.data,
}));
})
.catch((err) => {
setMovieData((oldMovieData) => ({
...oldMovieData,
isLoading: false
error: err,
}));
});
}
}, [movieId]);
return movieData;
};

Combining global redux store with local state of the component

The challenge I came across is using global store slice, namely 'genres', which is an array of objects, in a local state to manipulate check/uncheck of the checkboxes. The problem occurs when I'm trying to use props.genres in the initial state. Looks like I'm getting an empty array from props.genres when the local state is initialized.
const Filters = (props) => {
const { genres, getSelected, loadGenres, getGenres, clearFilters } = props
const [isChecked, setIsChecked] = useState(() =>
genres.map(genre => (
{id: genre.id, value: genre.name, checked: false}
))
)
const optionsSortBy = [
{name: 'Popularity descending', value: 'popularity.desc'},
{name: 'Popularity ascending', value: 'popularity.asc'},
{name: 'Rating descending', value: 'vote_average.desc'},
{name: 'Rating ascending', value: 'vote_average.asc'},
]
const d = new Date()
let currentYear = d.getFullYear()
let optionsReleaseDate = R.range(1990, currentYear + 1).map(year => (
{name: year + '', value: year}
))
useEffect(() => {
const url = `${C.API_ENDPOINT}genre/movie/list`
loadGenres(url, C.OPTIONS)
}, [])
const handleCheckbox = (e) => {
let target = e.target
getGenres(target)
}
const handleSelect = (e) => {
let target = e.target
let action = isNaN(target.value) ? 'SORT_BY' : 'RELEASE_DATE'
getSelected(action, target)
}
const handleSubmitBtn = (e) => {
e.preventDefault()
clearFilters()
}
return (
<form className={classes.FiltersBox}>
<Submit submited={handleSubmitBtn} />
<Select name="Sort By:" options={optionsSortBy} changed={handleSelect} />
<Select name="Release date:" options={optionsReleaseDate} changed={handleSelect} />
<Genres genres={isChecked} changed={handleCheckbox} />
</form>
)
}
const mapStateToProps = (state) => {
return {
genres: state.fetch.genres,
}
}
const mapDispatchToProps = (dispatch) => {
return {
loadGenres: (url, options) => dispatch(A.getApiData(url, options)),
getGenres: (targetItem) => dispatch({
type: 'CHECK_GENRES',
payload: targetItem
}),
getSelected: (actionType, targetItem) => dispatch({
type: actionType,
payload: targetItem,
}),
clearFilters: () => dispatch({type: 'CLEAR_FILTERS'})
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Filters);
import * as R from 'ramda';
import fetchJSON from '../utils/api.js';
export const getApiData = (url, options) => async (dispatch) => {
const response = await fetchJSON(url, options)
const data = response.body
const dataHas = R.has(R.__, data)
let actionType = dataHas('genres') ? 'FETCH_GENRES' : 'FETCH_MOVIES'
dispatch({
type: actionType,
payload: data
})
}
export const fetchReducer = (state = initialState, action) => {
const { payload } = action
if (action.type === 'FETCH_GENRES') {
return {
...state,
isLoading: false,
genres: [...payload.genres]
}
}
if (action.type === 'FETCH_MOVIES') {
return {
...state,
isLoading: false,
movies: [...payload.results]
}
}
return state
}
What you are trying to do of setting initial value for state from props, is possible but isn't react best practice. Consider initial your data as empty array and through useEffect manipulate state
// didn't understand if its array or bool
const [isChecked, setIsChecked] = useState([])
useEffect(()=>genres&& { setIsChecked(... perform action...)
} ,[genres])
You approach is almost correct.
I am not sure how the state should look like, when you have fetched your data.
I can see in the mapStateToProps is trying to access a value which is not defined at the beginning. If state.fetch is undefined you can not access genres.
Attempt 1:
You can solve it by using lodash.get https://lodash.com/docs/#get
It will catch up for the undefined problem.
Attempt 2:
You can defined an initial state where your values are defined with mock data.
const initialState = {fetch: {genres: []}}
and use it your reducer

Reactjs, class component to hooks

I'm still beginner with ReactJs. Actually I want to rewrite my class components to hook components but I have a problem with one part of my code. Anyone can help me with rewrite this component to hook?
This is my code:
class App extends Component {
state = {
selected: {},
data: data,
filtered: data
};
handleChange = data => {
if (data == null) {
this.setState({
filtered: this.state.data
});
} else {
this.setState({
selected: data,
filtered: this.state.data.filter(d => d.client_id === data.id)
});
}
};
returnClientNameFromID = id => options.find(o => o.id === id).name;
render() {
const {
state: { selected, data, filtered },
handleChange
} = this;
return ( <div>
...
Here's what you could do. With useState you always have to merge objects yourself setState((prevState) => {...prevState, ... })
const App = () => {
const [state, setState] = useState({
selected: {},
data: data,
filtered: data
})
const handleChange = data => {
if (data == null) {
setState((prevState) => {
...prevState,
filtered: this.state.data
});
} else {
setState((prevState) => {
...prevState,
selected: data,
filtered: prevState.data.filter(d => d.client_id === data.id)
});
}
};
const returnClientNameFromID = id => options.find(o => o.id === id).name;
const { selected, data, filtered } = state
return() (
<div> ... </div>
)
}

How can I access current redux state from useEffect?

I have a list of objects ("Albums" in my case) fetched from the database. I need to edit these objects.
In the editing component in the useEffect hook I fire up the action for getting the needed album using it's ID. This action works. However in the same useEffect I am trying to fetch the changed by before fired action redux state. And now I face the problem - all I am fetching is the previos state.
How can I implement in the useEffect fetching of current redux state?
I've seen similar questions here, however none of the answers were helpfull for my use case.
I am using redux-thunk.
Editing component. The problem appears in setFormData - it's fetching previous state from the reducer, not the current one. It seems that it fires before the state gets changed by the getAlbumById:
//imports
const EditAlbum = ({
album: { album, loading},
createAlbum,
getAlbumById,
history,
match
}) => {
const [formData, setFormData] = useState({
albumID: null,
albumName: ''
});
useEffect(() => {
getAlbumById(match.params.id);
setFormData({
albumID: loading || !album.albumID ? '' : album.albumID,
albumName: loading || !album.albumName ? '' : album.albumName
});
}, [getAlbumById, loading]);
const { albumName, albumID } = formData;
const onChange = e =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmit = e => {
e.preventDefault();
createAlbum(formData, history, true);
};
return ( //code );
};
EditAlbum.propTypes = {
createAlbum: PropTypes.func.isRequired,
getAlbumById: PropTypes.func.isRequired,
album: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
album: state.album
});
export default connect(
mapStateToProps,
{ createAlbum, getAlbumById }
)(withRouter(EditAlbum));
Action:
export const getAlbumById = albumID => async dispatch => {
try {
const res = await axios.get(`/api/album/${albumID}`);
dispatch({
type: GET_ALBUM,
payload: res.data
});
} catch (err) {
dispatch({
type: ALBUMS_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
reducer
const initialState = {
album: null,
albums: [],
loading: true,
error: {}
};
const album = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_ALBUM:
return {
...state,
album: payload,
loading: false
};
case ALBUMS_ERROR:
return {
...state,
error: payload,
loading: false
};
default:
return state;
}
};
Will be grateful for any help/ideas
You should split up your effects in 2, one to load album when album id changes from route:
const [formData, setFormData] = useState({
albumID: match.params.id,
albumName: '',
});
const { albumName, albumID } = formData;
// Only get album by id when id changed
useEffect(() => {
getAlbumById(albumID);
}, [albumID, getAlbumById]);
And one when data has arrived to set the formData state:
// Custom hook to check if component is mounted
// This needs to be imported in your component
// https://github.com/jmlweb/isMounted
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
// In your component check if it's mounted
// ...because you cannot set state on unmounted component
const isMounted = useIsMounted();
useEffect(() => {
// Only if loading is false and still mounted
if (loading === false && isMounted.current) {
const { albumID, albumName } = album;
setFormData({
albumID,
albumName,
});
}
}, [album, isMounted, loading]);
Your action should set loading to true when it starts getting an album:
export const getAlbumById = albumID => async dispatch => {
try {
// Here you should dispatch an action that would
// set loading to true
// dispatch({type:'LOAD_ALBUM'})
const res = await axios.get(`/api/album/${albumID}`);
dispatch({
type: GET_ALBUM,
payload: res.data
});
} catch (err) {
dispatch({
type: ALBUMS_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
Update detecting why useEffect is called when it should not:
Could you update the question with the output of this?
//only get album by id when id changed
useEffect(() => {
console.log('In the get data effect');
getAlbumById(albumID);
return () => {
console.log('Clean up get data effect');
if (albumID !== pref.current.albumID) {
console.log(
'XXXX album ID changed:',
pref.current.albumID,
albumID
);
}
if (getAlbumById !== pref.current.getAlbumById) {
console.log(
'XXX getAlbumById changed',
pref.current.getAlbumById,
getAlbumById
);
}
};
}, [albumID, getAlbumById]);

Resources