Introduction
In am implementing an Instagram clone:
Users have posts.
Posts can be liked.
Users can comment in posts.
Posts have a totalLikes and totalComments field.
Comments can be liked.
Users can reply to comments.
Comments have a totalLikes and totalComments (replies) field.
Until now, I have been working with the current PostsContext:
const initialState = {};
export function PostsProvider({ children }) {
const [contents, dispatch] = useReducer(contentsReducer, initialState);
const addUserPosts = (userId, posts, unshift = false, cached = true) => { ... }
const likePost = (postOwnerId, postId) => { ... }
const commentPost = (postOwnerId, postId, comment) => { ... }
const getUserPosts = (userId) => { ... }
const getUserPost = (userId, postId) => { ... }
}
Where, my stateful data looks like:
{
userId1: {
posts: [
{
id,
uri,
totalLikes,
totalComments,
isLiked,
date
},
...
]
},
userId2: { posts: [...] }
}
Question
My question is, should I use another context for comments? I mean, comments will work the same way as posts, they can be liked and replied... so maybe, it is unnecessary.
When a user comments in a post:
1. The post totalComments is increased.
2. If the user is replying to a comment in the post, the replied comment's totalComments is increased too.
When a user likes a comment, it doesn't affect to the post "data" itself, only to the liked comment totalLikes field.
So... if I create a new context for comments, it seems that I will need to consume the PostsContext inside it, in order to increase the post totalComments field.
Any ideas about how to structure my posts context?
Note: My question is more about the context organization than anything else (do not look at implementation, just to the structure of my stateful data and the idea of splitting contexts).
There are trade-offs. With multiple contexts you have the option of having a component rendered within a single context, thus reducing refreshes (increasing performance) when another context changes.
However, if most of your components need access to multiple contexts, it may work better to have a "store" (single app-level context). If you're using the selector pattern (which is not just for Redux), then a selector can compute data that would otherwise be in multiple contexts.
I am working on a project with multiple contexts, and regularly think about combining them.
If your project is complex, you may want to consider Redux instead of contexts. It adds complexity but solves some of the issues around organization and combining different areas of the state. If you do that I highly recommend starting with Redux Toolkit to reduce the boilerplate.
Related
I am working with ReactJS and Google Firestore. I have a component called GameEntryForm, where you can select from a list of users stored in Firestore. In order to get this list, when I render the GameEntryForm component, I make a query to Firestore. Below is how I am getting the list.
I was wondering if there was a better or faster way to do this. My concern is that as the number of users increases, this could be a slow operation.
function GameEntryForm() {
// prevent rendering twice
const effectRan = useRef(false);
const [usersList, setUsersList] = useState(new Map());
useEffect(() => {
if (effectRan.current === false) {
const getUsers = async () => {
const q = query(collection(firestore, "users"));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
setUsersList(new Map(usersList.set(doc.data().uid, doc.data())));
});
};
getUsers();
return () => {
effectRan.current = true;
};
}
}, []);
}
Your code looks fine at first glance, but
here are many ways to mitigate this issue some of them are as follows:
Implement Pagination Functionality to limit the number of documents that are returned by the query, for more about this topic go through this docs
Use Firestore Offline Caching feature through persistence like one provided here. I understand that your user will be added constantly so there’s not much improvement with this method but you can trigger a new request to the db based on the changed type. This is nicely explained in this thread
You can also use the above caching with a global state management solution(Redux, Context API) and only fetch the list of users once. This way, the list of users would be accessible to all components that need it, and you would only have to make the query once. Someone has created an example for how this will work although not using firestore though.
Last but not least use Real Time lister to View changes between snapshots as provide here in official docs This works great with the offline Caching option.
INTRODUCTION
In my app I have multiple contexts for managing and storing state.
In one of my scenarios, I have noticed that, as I have a UsersContext (which stores the users data), it is not necessary to store the user data of each post creator in my PostsContext, it could be better (for RAM) to just store its user id, and not to repeat the user data on it.
I have thought to use "parsers" inside my reducers logic before storing data, in order to remove the unnecessary fields of each object.
Is this a good idea / is a common pattern?
PROBLEM
The main problem I notice with this approach is: how to get the data correctly from both contexts? I mean, imagine the following component:
function Card({ content: { userData, image, description, location, date }) {
...
}
If I want to get the data from the contexts, and not from props, I have implemented two custom hooks (do not care about memoizations right now) which consumes a specific context:
/* HOOKS FOR GETTING SPECIFIC DATA FROM CONTEXT */
function useUpdatedPostData(postData) {
const posts = usePosts(); <-- consume PostsContext
return {
...postData,
...posts.getPost(postData.id) <--- Merging with data from context
}
}
function useUpdatedUserData(userData) {
const users = useUsers(); <-- consume UsersContext
return {
...userData,
...users.getUser(userData.id) <--- Merging with data from context
}
}
/* CONSUMER COMPONENT */
function Card({ content }) {
// The main problem:
const {
image,
description,
location,
date,
} = useUpdatedPostData(content);
const { username, avatar } = useUpdatedUserData(content.userData);
...
}
something which makes the code really difficult to read. Any tips?
I always run into situations where I need to force rerender, while I'm still in the execution of some function, so I developed my solution to this and I need to know if this is right or there is a simpler way to achieve the same goal.
I rely on the state variable my_force_update, then I change it to a random value when I want to enforce a change. like:
const [my_force_update, setMyForceUpdate] = useState(0);
useEffect(()=>{}, [my_force_update]);
const handleSubmit = async () =>{
await prm1();
stMyForceUpdate(Math.random()); // enforcing the effect
await prom2();
....
}
so I have been able to enforce re-render (by enforcing the effect) while I'm still in the handleSubmit execution.
is there a simpler way? or, did I mistakenly understand the concepts of React?
update
The issue is that I have a checkout form, and I need it to be a signup form at the same time, and there is also a login component on the page.
so I need to populate the form fields with the account if information in case of login and in case of sign up.
The steps are as follow:
if user login => populate form (per fill it with user info) => move to payment.
if user fill out the form manually:
create an account.
authenticate the new user.
update the user account.
repopulate form (with data from user account).
move to payment.
so I have this function that needs to listen to the login and signup:
const token = useSelector(_token);
const loggedIn = useSelector(_loggedIn);
const profile = useSelector(_profile);
useEffect(() => {
/**
* Pre-fill the form inputs
*/
(async () => {
const r = await dispatch(fetchUserInfo());
setFormProfile(profile); // address is not updated yet
setFormAddress(r?.result?.address);
})();
}, [loggedIn, forceUpdate]);
now, there are no issues with the login process, the only problem is with the signup:
at step 2, when authenticating the user, its account is empty.
so the loggedIn changes to true when the profile is empty so I got empty form.
after updating the profile, loggedIn will not change, so I need another variable to trigger the effect again.
I tried to listen to profile here, but I got an infinite loop.
and here is the checkout flow related to the signup:
...
if (!loggedIn) {
const signupResponse = await dispatch(signupUser(params));
loginResponse = await dispatch(login(formProfile?.email, password));
}
const updateProfileResponse = await saveChangesToProfile();
// update user profile with the information in the checkout form.
...
then save changes to the profile:
const saveChangesToProfile = async () => {
const r = await dispatch(fetchUserInfo());
const addressID = r?.result?.address_id;
const res1 = await dispatch(updateUserAddress(addressID, { ID: addressID, ...formAddress }));
const res = await dispatch(UpdateUser(r?.result?.ID, formProfile));
setForceUpdate(Math.random()); // force re-render to re-populate the form.
setSuccess("Information saved to your profile!");
return res;
};
Update 2
The question is general, I solved the issue in another way days ago (involving changes to the server routes). and I'm asking the question in a general way to get some knowledge, not for others to do the work for me.
In general, you should avoid having to force an update in React but instead use existing React features to accomplish your goal. That being said, there are simple ways to force a re-render in react. You mentioned in the second update that you are looking for more general solutions - so I will provide them here.
However, please bear in mind that this topic has been discussed extensively in other stack overflow questions (I will provide links).
Forcing Re-Render using component.forceUpdate(callback)
The react docs actually list a simple way to force a component to reload (provided you maintain a reference to it). You can find more information here, but essentially it forces your component to re-render and then makes a call to the callback argument.
Forcing Re-Render using hooks
There are multiple stack overflow questions that provide simple code snipets that can force a react component to re-render by using hooks. This answer for example by #Qwerty demonstrates 2 simple code snipets to force a re-render:
const forceUpdate = React.useState()[1].bind(null, {}) // see NOTE above
const forceUpdate = React.useReducer(() => ({}))[1]
You should check out his answer for a more detailed explanation.
Other sources include this answer to the same stack overflow question that references the official FAQ.
It solves the problem by doing:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
Solving Your Specific Problem
I saw that you were able to solve your problem by using the useEffect hook - a great start for a potential solution. You also mentioned that you got an infinite loop while listening to a variable change in your hook - a common problem and one with some common solutions. In general, you should always run a check inside the useEffect hook before changing any of its dependencies. For example, run a check to see if the profile is unset before trying to update its value.
I however would recomend that you use a progress varible that would indicate your status, something like this:
const STATUS_START = 0;
const STATUS_LOGED_IN = 1;
const STATUS_SIGNING_UP = 2;
const [progress, setProgress] = useState(STATUS_START);
Then, you can simply listen to changes made to the progress variable in your useEffect hook (by passing it as your only dependent). This should automatically condition you to write the necessary logic to check for state inside of the useEffect function as I described previously.
This solution would work by initially setting the progress to either signing up or logging in, but only filling the form data if you are logged in (and after the signup progress is done calling setProgress(STATUS_LOGED_IN))
i'm new to react please forgive me if i'm asking a dumb question.
The idea is to access the tweets array from context, find the matching tweet and then set it in the component's state to access the data.
However, the tweets array results empty even though i'm sure it's populated with tweets
const { tweets } = useContext(TweeetterContext)
const [tweet, setTweet] = useState({})
useEffect(() => {
loadData(match.params.id, tweets)
}, [])
const loadData = (id, tweets) => {
return tweets.filter(tweet => tweet.id == id)
}
return (stuff)
}
You are accessing context perfectly fine, and it would be good if you could share a code where you set tweets.
Independent of that, potential problem I might spot here is related to the useEffect function. You are using variables from external context (match.params.id and tweets), but you are not setting them as dependencies. Because of that your useEffect would be run only once at the initial creation of component.
The actual problem might be that tweets are set after this initial creation (there is some delay for setting correct value to the tweets, for example because of the network request).
Try using it like this, and see if it fixes the issue:
useEffect(() => {
loadData(match.params.id, tweets)
}, [match.params.id, tweets])
Also, not sure what your useEffect is actually doing, as it's not assigning the result anywhere, but I'm going to assume it's just removed for code snippet clarity.
I am working on a music application that is split between several features:
library: displays the available content and allows the user to browse artists and albums, and play one or a few tracks. The state looks like this:
library {
tracksById: {...}
albumsById: {...}
artistsById: {...}
albums: []
artists: []
}
player: the part that plays the music. Just contains an array of all tracks to be played. Also shows a list of the tracks to be played
and etc; The problem is that I would like to share tracksById, albumsById and artistsById between all features, because each album has an artist property, but this property is just an id, so if I want to display the artist name near the track name in the playlist, I need to fetch the artist, and put it in my store. I could simply connect them both like this:
#connect(createStructuredSelector({
player: (state) => state.player,
library: (state:) => state.library
}), (dispatch) => ({
actions: bindActionCreators(actions, dispatch)
}))
And then in my playlist view:
<div className="artist-name">{this.library.artistsById[track.artist].name}</div>
However this increases coupling between features and I find that it defeats the very purposes of multiple reducers.
Another way would be to create a new feature content, which would only contain the actions and byId properties needed with the most minimal reducer possible and this one would be shared by other features. However, this means I have to add a new prop to every component content, and I need to add the actions.
I could also create a service, or even maybe a function that would take a a reducer and actions object, and add a logic to make it able to fetch artists, albums and tracks and save them in the store. I really like this idea since it decreases coupling, however, it means duplicated data in my state, which means more memory used.
I am not looking for the best way, just wondering if there is any other and what are the pros and cons of each method
I'm pretty sure I don't fully understand your question, but I'll share some thoughts. Are you trying to avoid duplicate queries or duplicated state data?
Objects are fast dictionaries
This bit of duplication:
{this.library.artistsById[track.artist].name}
becomes unnecessary if library.artists is an object instead of an array. Assume 1 and 2 are artist IDs, artists would use the artist ids as keys in the object:
{
1: {name: 'Bob Marley', ...etc},
2: {name: 'Damien Marley', ...etc}
}
Which lets you get artist info from library.artists using only the id:
{this.library.artists[track.artistId].name}
Use selector functions
But what you really want is getArtistById(state, artistId){}, a selector function in your reducer code file. That lets you rearrange the state any time and none of your view components will be affected. If artists is an object with the keys being artist IDs, the implementation could look like this:
getArtistById(state, artistId){
return state.library.artists[artistId]
}
I agree with your observation that accessing library.artistsById[track.artist].name adds unnecessary coupling. I would never put code that knows about high-level state shape into a view component.
For your example playlist view with a list of artists, each item in the playlist view would be its own tiny component. The container would look like this:
import { getArtistById } from '../reducers/root-reducer'
const mapStateToProps = (state) => {
const artist = getArtistById(state.library)
return { artistName: artist ? artist.name : '' }
}
...
The tiny view component would look something like:
render() {
const {artistName} = this.props
return <div className="artist-name">{artistName}</div>
}
Premature optimization?
You might be prematurely optimizing. Even if you keep artists and albums as arrays, your selector function can search through the array for the ID. A function to get each of those by id from a state that only contains { artists: [], albums: [], tracks:[] } should be very fast until the library got pretty huge.