I have a like function on the backend (Node, MongoDB) that returns the given post with updated likes counter. This works, tested it with Postman. This is just an object with a bunch of properties like likes, _id., by, createdAt and so on...
let p = await Post.findById(req.params.id).populate("by");
return res.json(p);
Then I have a like action in React:
export const like = (id) => (dispatch) => {
const token = localStorage.getItem("token");
if (token) {
axios
.put(`http://localhost:5000/likePost/${id.id}`, id, {
headers: { "X-Auth-Token": token },
})
.then((res) => {
dispatch({
type: LIKE,
payload: res.data,
});
});
}
};
And I have a LIKE reducer:
case LIKE:
return {
...state,
posts: state.posts.map((p) => {
return { ...p };
}),
};
The LIKE reducer triggers when I click on the button and on the backend I can see the update but on the client side it doesn't update. I use redux-logger and the posts state is not updated.
What did I do wrong? I thought that spreading all the posts (...p) will update it, since it is updated on the backend.
This one works:
case LIKE:
return {
...state,
posts: state.posts.map((p) => {
if (p._id === action.payload._id) {
p.likes = action.payload.likes;
}
return p;
}),
};
Related
Take a look at my Slice below
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
const KEY = process.env.REACT_APP_API_KEY
const BASE_URL = process.env.REACT_APP_BASE_URL
const API = `${BASE_URL}/countrymap`
const initialState = {
mapdata:[],
status: 'idle',
error:null
}
export const fetchMapData = createAsyncThunk(
'mapdata/fetchMapData',
async (id) => {
try {
const response = await axios.get(
API,
{
headers: {
'Content-Type': 'application/json',
'X-API-KEY': KEY,
},
params: {
titleId: id,
}
}
)
return response.data.Item;
} catch (error) {
console.error('API call error:', error.message);
}
}
)
const mapSlice = createSlice({
name: 'mapdata',
initialState,
reducers:{
},
extraReducers(builder) {
builder
.addCase(fetchMapData.fulfilled, (state, action) => {
state.status = 'succeeded'
//This attempt to make it an array failed with the error
// that the state is not iterable
state.mapdata = [action.payload, ...state]
console.log("map payload: ", state.mapdata);
})
}
})
// SELECTORS
// This works for an array, but the data is originally coming
// in as an object instead
export const allMapData = (state, id) => state.mapdata.mapdata.find(item => item.title_id === id);
export default mapSlice.reducer
for reference, look at these two console logs from two different API calls to two different endpoints from two different slices . Except Media is coming back as an array, and map is an object
I need to either, turn the state.mapdata into an Object to I can use the selector the way it is or re-code the selector so that it doesn't use the .find() function because that's an array function. But either way, it needs to compare the titleId in the data to the id in the params
Sorry for not providing workable code. I would but there is an insane amount of dependencies here
You should use
state.mapdata = [action.payload, ...state.mapdata]
instead of
state.mapdata = [action.payload, ...state]
I have an AsyncThunk method named likePost when a user clicks like on a post it will send this action via dispatch. The method runs fine, and in my database the Post is updated successfully, but I can't get Redux to update the Post that is liked during the .fulfilled method.
Here is what I'm currently working with:
// shoes = [{_id: '', title: '', likes: []}, ...{}]
export const likePost = createAsyncThunk(
'posts/likePost',
async (payload, { rejectWithValue }) => {
try {
const response = await userTokenAxios.post(`/${payload}/like`);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
[likePost.fulfilled]: (state, action) => {
const post = state.posts.find((post) => post._id === action.payload._id);
post.likes.push(action.payload.user);
},
Instead of finding the post, get the index for that object in state.posts
const post = state.posts.find((post) => post._id === action.payload._id);
const postIndex=state.posts.findIndex((post)=> post._id === action.payload._id);
Now you can push the user in likes array:
state.posts[postIndex].likes.push(action.payload.user)
Here I am trying to update my state data with a reducer. I am sending an object through dispatch which is consisted of id, and some data. By this id, I am trying to update a specific object of my state. My state reducer is,
case "UPDATE_USER_SUCCESS":
return {
...state,
users: state.users.map((user) => {
if (user.id !== action.payload.id) {
return user;
} else {
return {
...user,
...action.payload,
};
}
}),
};
Here is the action,
export const updateUser = (updatedUser) => (dispatch, getState) => {
dispatch({
type: "UPDATE_USER_SUCCESS",
payload: updatedUser,
});
console.log(updateUser);
localStorage.setItem(
"Users",
JSON.stringify(getState().addUserReducer.users)
);
};
ANd my dispatching is from here,
const formHandler = (e) => {
e.preventDefault();
dispatch(updateUser(updatedUser));
console.log(name + email + phone + roles);
};
return {
...user,
...action.payload,
};
what is your goal here? If user was an array, by utilizing this code , you would add action.payload to your array. But user is an object. If you want to pass another object to it, you have to give it a key:
return {
...user,
whateverkey:action.payload,
};
and your user objcect has a new key-value pair.
furthermore: if you want easier access further on you can use this as well:
return {
...user,
id:action.payload.id,
name:action.payload.name,
address:action.payload.address
};
I have a function "sendMessage" in React class:
class MessageForm extends React.Component {
...
sendMessage = async () => {
const { message } = this.state;
if (message) {
this.setState({ loading: true });
if (this.props.isPrivateChannel === false) {
socket.emit("createMessage", this.createMessage(), (response) => {
this.setState({ loading: false, message: "", errors: [] });
});
} else {
if (this.state.channel && this.state.channel._id === undefined) {
socket.emit("createChannelPM", this.state.channel, async (response) => {
const chInfo = { ...response, name: this.props.currentChannel.name };
console.log("chInfo : ", chInfo);
await this.props.setCurrentChannel(chInfo).then((data) => {
if (data) {
console.log("data : ", data);
console.log("this.props.currentChannel : ", this.props.currentChannel);
}
});
});
}
...
function mapStateToProps(state) {
return {
isPrivateChannel: state.channel.isPrivateChannel,
currentChannel: state.channel.currentChannel,
};
}
const mapDispatchToProps = (dispatch) => {
return {
setCurrentChannel: async (channel) => await dispatch(setCurrentChannel(channel)),
}
};
Here, in sendMessage function, I retrieve "response" from socket.io, then put this data into variable "chInfo" and assign this to Redux state, then print it right after assinging it.
And Redux Action function, "setCurrentChannel" looks like:
export const setCurrentChannel = channel => {
return {
type: SET_CURRENT_CHANNEL,
payload: {
currentChannel: channel
}
};
};
Reducer "SET_CURRENT_CHANNEL" looks like:
export default function (state = initialState, action) {
switch (action.type) {
case SET_CURRENT_CHANNEL:
return {
...state,
currentChannel: action.payload.currentChannel
};
...
The backend Socket.io part look like (I use MongoDB):
socket.on('createChannelPM', async (data, callback) => {
const channel = await PrivateChannel.create({
...data
});
callback(channel)
});
The console.log says:
Problem : The last output, "this.props.currentChannel" should be same as the first output "chInfo", but it is different and only print out previous value.
However, in Redux chrome extension, "this.props.currentChannel" is exactly same as "chInfo":
How can I get and use newly changed Redux states immediately after assinging it to Redux State?
You won't get the updated values immediately in this.props.currentChannel. After the redux store is updated mapStateToProps of MessageForm component is called again. Here the state state.channel.currentChannel will be mapped to currentChannel. In this component you get the updated props which will be accessed as this.props.currentChannel.
I believe you want to render UI with the latest data which you which you can do.
What I am trying to do
I have a lobby that users can join. To persist the joined lobby on the client on a page refresh I decided to put the lobby that has been joined into the browser's session storage. Before it was just in a useState which doesn't persist through a page refresh.
Setting Session Storage is classified as a side effect as far as I know and should be handled in useEffect. The problem is when I set the lobby the useEffect that has the lobby as a dependency doesn't run.
Setting breakpoints shows that it doesn't run at all, but I can see that the joinedLobby has changed from undefined to an object (example : {success: "Successfully joined ...", payload : { id:"", ...}}).
The session store stays empty.
Code Sandbox
Sandbox
CSS is broken since I was using Emotion
Update
Fetching Data from the back end breaks the app. Making the data static made the app function like it should.
I have 0 ideas on why / how. The culprit seems to be play_index.jsx at line 165 const jsonResponse.
Setting the state that should update the useEffect
const { setJoinedLobby } = useContext(JoinedLobbyProviderContext);
const history = useHistory();
useEffect(() => {
if (joinState.result === undefined) return;
setJoinedLobby(joinState.result);
history.push('/lobby');
}, [joinState.result, history, setJoinedLobby]);
Provider inside router
<JoinedLobbyProviderContext.Provider
value={{ getJoinedLobby, setJoinedLobby }}>
<Route path='/play'>
<Play />
</Route>
<Route path='/lobby'>
<Lobby />
</Route>
</JoinedLobbyProviderContext.Provider>
The functions the provider takes
const [joinedLobby, setJoinedLobby] = useState(undefined);
useEffect(() => {
if (joinedLobby === undefined) return;
sessionStorage.setItem('joinedLobby', JSON.stringify(joinedLobby));
}, [joinedLobby]);
const getJoinedLobby = () => {
return JSON.parse(sessionStorage.getItem('joinedLobby'));
};
Edit : How joinState.result changes
const joinInit = {
errors: undefined,
loading: false,
result: undefined,
id: undefined,
};
const joinReducer = (state, action) => {
switch (action.type) {
case 'joinLobby': {
return { ...state, id: action.payload };
}
case 'loadingTrue':
return { ...state, loading: true };
case 'setResult':
return { ...state, loading: false, result: action.payload };
case 'setErrors':
return {
...state,
loading: false,
errors: action.payload,
};
case 'reset':
return joinInit;
default : {throw new Error('Didn't find action passed to reducer')}
}
};
const [joinState, joinStateDispatch] = useReducer(joinReducer, joinInit);
const passRef = useRef();
useEffect(() => {
const joinLobby = async () => {
joinStateDispatch({ type: 'loadingTrue' });
try {
const jsonResponse = await (
await fetch(`${BACKEND_URL}/play/joinLobby/${joinState.id}`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify({
password: passRef.current.value,
}),
})
).json();
joinStateDispatch({ type: 'setResult', payload: jsonResponse });
} catch (e) {
joinStateDispatch({ type: 'setErrors', payload: e });
}
};
if (joinState.id !== undefined) {
joinLobby();
}
}, [joinState.id, joinStateDispatch]);