React - Reducing context data size using multiple contexts - javascript

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?

Related

How to structure a React Context for posts that have comments

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.

Returning a value from an Async Function. AWS/React

I'm trying to build a component that retrieves a full list of users from Amazon AWS/Amplify, and displays said results in a table via a map function. All good so far.
However, for the 4th column, I need to call a second function to check if the user is part of any groups. I've tested the function as a button/onClick event - and it works (console.logging the output). But calling it directly when rendering the table data doesn't return anything.
Here is what I've included in my return statement (within the map function)
<td>={getUserGroups(user.email)}</td>
Which then calls this function:
const getUserGroups = async (user) => {
const userGroup = await cognitoIdentityServiceProvider.adminListGroupsForUser(
{
UserPoolId: '**Removed**',
Username: user,
},
(err, data) => {
if (!data.Groups.length) {
return 'No';
} else {
return 'Yes';
}
}
);
};
Can anyone advise? Many thanks in advance if so!
Because you should never do that! Check this React doc for better understanding of how and where you should make AJAX calls.
There are multiple ways, how you can solve your issue. For instance, add user groups (or whatever you need to get from the backend) as a state, and then call the backend and then update that state with a response and then React will re-render your component accordingly.
Example with hooks, but it's just to explain the idea:
const [groups, setGroups] = useState(null); // here you will keep what "await cognitoIdentityServiceProvider.adminListGroupsForUser()" returns
useEffect(() => {}, [
// here you will call the backend and when you have the response
// you set it as a state for this component
setGroups(/* data from response */);
]);
And your component (column, whatever) should use groups:
<td>{/* here you will do whatever you need to do with groups */}</td>
For class components you will use lifecycle methods to achieve this (it's all in the documentation - link above).

Is it possible to use references as react component's prop or state?

I want to create react table component which values are derived from single array object. Is it possible to control the component from view side? My goal is that every user using this component in their web browsers share the same data via singleton view object.
Program modeling is like below.
Database - there are single database in server which contain extinct and independent values.
DataView - there are singleton View class which reflects Database's table and additional dependent data like (sum, average)
Table - I'll build react component which looks like table. And it will show View's data with supporting sorting, filtering, editing and deleting row(s) feature (and more). Also it dose not have actual data, only have reference of data from View(Via shallow copy -- This is my question, is it possible?)
My intentions are,
- When user changes value from table, it is queried to DB by View, and if succeed, View will refer updated data and change it's value to new value and notify to Table to redraw it's contents. -- I mean redraw, not updating value and redraw.
- When values in View are changed with DB interaction by user request, there are no need to update component's value cause the components actually dose not have values, only have references to values (Like C's pointer). So only View should do is just say to Component to redraw it's contents.
I heard that React's component prop should be immutable. (Otherwise, state is mutable) My goal is storing references to component's real value to it's props so that there are no additional operation for reflecting View's data into Table.
It is concept problems, and I wonder if it is possible. Since javascript dose not support pointer officially(Am I right?), I'm not sure if it is possible.
View class is like below,
const db_pool = require('instantiated-singleton-db-pool-interface')
class DataView {
constructor() {
this.sessions = ['user1', 'user2'] // Managing current user who see the table
this.data = [ // This is View's data
{id:1, name:'James', phone:'12345678', bank:2000, cash:300, total:2300,..},
{id:2, name:'Michael', phone:'56785678', bank:2500, cash:250, total:2300,..},
{id:3, name:'Tyson', phone:'23455432', bank:2000, cash:50, total:2300,..}
] // Note that 'total' is not in db, it is calculated --`dependent data`
}
notifySessionToUpdate(ids) {
// ids : list of data id need to be updated
this.sessions.forEach((session) => {
session.onNotifiedUpdateRow(ids) // Call each sessions's
})
}
requestUpdateRow(row, changed_value) {
// I didn't write async, exception related code in this function for simple to see.
update_result = db_pool.update('UPDATE myTable set bank=2500 where id=1')
if (update_result === 'fail') return; // Do Nothing
select_result = db_pool.select('SELECT * from myTable where id=1') // Retrieve updated single data which object scheme is identical with this.data's data
for (k in Object.keys(select_result)) {.ASSIGN_TO_row_IF_VALUE_ARE_DIFFERENT.} // I'm not sure if it is possible in shallow copy way either.
calc.reCalculateRow(row) // Return nothing just recalculate dependant value in this.data which is updated right above.
// Notify to session
this.notifySessionToUpdate([1]) // Each component will update table if user are singing id=1's data if not seeing, it will not. [1] means id:1 data.
return // Success
}
... // other View features
}
Regarding session part, I'm checking how to implement sessionizing(?) the each user and it's component who is communicating with server. So I cannot provide further codes about that. Sorry. I'm considering implementing another shallow copied UserView between React Component Table and DataView(And also I think it helps to do something with user contents infos like sorting preference and etc...)
Regarding DB code, it is class which nest it's pool and query interface.
My problem is that I'm not familiar with javascript. So I'm not sure shallow copy is actually implementable in all cases which I confront with.
I need to think about,
1. Dose javascript fully support shallowcopy in consistent way? I mean like pointer, guarantee check value is reference or not.
2. Dose react's component can be used like this way? Whether using props or state Can this be fullfilled?
Actually, I strongly it is not possible to do that. But I want to check your opinions. Seems it is so C language-like way of thinking.
Redraw mean re-render. You can expose setState() or dispatch() functions from Table component and call them on View level using refs:
function View() {
const ref = useRef();
const onDbResponse = data => ref.current.update(data);
return (
<Table ref={ ref } />
);
}
const Table = React.forwardRef((props, ref) => {
const [ data, setData ] = useState([]);
useImperativeHandler(ref, {
update: setData
});
...
});
Anyway i don't think it's a good practice to update like that. Why can't you just put your data in some global context and use there?
const Context = React.createContext({ value: null, query: () => {} });
const Provider = ({ children }) => {
const [ value, setValue ] = useState();
const query = useCallback(async (request) => {
setValue(await DB.request(request));
}, [ DB ]);
const context = { value, query };
return <Context.Provider value={ context }>{ children }</Context.Provider>;
}
const useDB = () => useContext(Context);
const View = () => {
const { request } = useDB();
request(...);
}
const Table = () => {
const { value } = useDB();
...
}

Filtering a list of names with ReactJS using data from RelayJS without calling GrpahQL

I'm stuck in wondering if I can filter a list of names which I receive from Relay and graphql-java server without the need of making calls, without making any changes in my GrpahQL schema and only using ReactJS for this purpose.
---MobX as a state management library can be a decision but I should first store all the Relay result.
Caveat emptor: I'm newish at Relay as well & struggling with these same concepts. But, given the relative dearth of accessible information on Relay, I thought it'd be helpful to try and layout the key concepts here. My understanding could be wrong, so I'd love it if anyone who found a mistake in my code/reasoning would comment/edit.
Filtering took awhile for me to 'click' as well. It depends on where you keep the data you'll use to filter, but let's assume the name field lives on your Users Type, and the query is something like this:
viewer {
allUsers {
edges {
node {
name
}
}
}
}
And let's say your top-level NameList component looked like this:
class NameList extends Component {
render() {
return (
<div>
{this.props.users.edges
.map(u => {
<NameItem name={u.node} />
})
}
</div>
)
}
}
Relay.createContainer(NameList, {
initialVariables: { first: 10 },
fragments: {
users: () => Relay.QL`
fragment on Viewer {
allUsers(first: $first) {
edges {
node {
${NameItem.getFragment('user')}
}
}
}
}
`
}
})
And your NameItem setup was simply:
class NameItem extends Component {
render() {
return (
<div>
Name: {this.props.user.name}
</div>
)
}
}
Relay.createContainer(NameItem, {
initialVariables: {},
fragments: {
user: () => Relay.QL`
fragment on User {
name
}
`
}
})
Consider the generalizable pattern here:
The List Component
A List component takes a fragment on the top-level Type in the query--in this case, Viewer, from a Relay container.
List also inserts a fragment on behalf of its Item child at the level of the User Type.
In other words, it captures an array of User objects it's supposed to pass down to the Item component.
If this wasn't Relay, and instead was, say, Redux, this component might simply pass state.users to the Item component. You can do that because, at some point, you've manually extracted all your Users from your own back-end and loaded them into Redux. But since Relay does the hard thinking for you, it needs a teensy bit more information than Redux.
The Item Component
This is even more simple. It expects an entity of type User and renders the name. Besides syntax, the functionality here isn't much different from a similar component in a Redux setup.
So really, without the complexity of Relay on top, all you have is an array of items that you're rendering. In vanilla React, you'd simply filter the array prior to (or during) your call to .map() in render().
However, with Relay, the fragment handed to the child is opaque to the parent--i.e., the List is handing a blind package to the Item, so it can't make a decision on whether or not to pass it down based on the fragment's content.
The solution in this contrived example is pretty simple: just peel-off the name field at the parent and child level. Remember: Relay is about components telling GraphQL what data they need. Your List component needs whatever fields it intends on filtering on--no more, no less.
If we modify the above List container:
...
users: () => Relay.QL`
fragment on Viewer {
allUsers(first: $first) {
edges {
node {
name
${NameItem.getFragment('user')}
}
}
}
}
`
And then we update our render function:
<div>
{this.props.users.edges
.map(u => {
if (u.node.name == "Harvey") {
<NameItem name={u.node} />
}
})
}
</div>
Then we've achieved basic filtering without needing mobx, more server trips, etc.

Flux + React.js - Callback in actions is good or bad?

Let me explain the problem that I've faced recently.
I have React.js + Flux powered application:
There is a list view of articles (NOTE: there are multiple of of different lists in the app) and article details view inside it.
But there is only one API endpoint per each list which returns array of articles.
In order to display the details I need to find article by id in array. That works pretty fine. I trigger action which makes request to server and propagates store with data, when I go to details screen then I just get the necessary article from that array in store.
When user lands on article details view before list (stores are empty) then I need to make a request.
Flow looks like: User loads details view -> component did mount -> stores are empty -> rendered empty -> fetchArticles action is triggered -> request response is 200 -> stores now have list of articles -> component did update -> rendered with data successfully
Component could look as follows:
let DetailsComponent = React.createClass({
_getStateFromStores() {
let { articleId } = this.getParams();
return {
article: ArticleStore.getArticle(articleId)
};
},
componentDidMount() {
// fire only if user wasn't on the list before
// stores are empty
if (!this.state.article) {
ArticleActions.fetchArticles('listType');
}
},
render() {
return <ArticleDetails article={this.state.article} />;
}
});
The interesting part comes next:
Now I need to make another request to server but request options depend on the article details. That's why I need to make second request after the first one on the details view.
I've tried several approaches but all of them look ugly. I don't like calling actions from stores that makes stores too complicated. Calling action inside action in this case doesn't work well because I will need to find article from store inside that action.
Solution (?!)
What I've came up with is to use callback in action inside component and it feels much more cleaner:
let DetailsComponent = React.createClass({
_getStateFromStores() {
let { articleId } = this.getParams();
return {
article: ArticleStore.getArticle(articleId)
};
},
componentDidMount() {
if (!this.state.article) {
ArticleActions.fetchArticles('listType', () => {
this._requestAdditionalData();
});
}
this._requestAdditionalData();
},
_requestAdditionalData() {
if (this.state.article) {
ArticleActions.fetchAdditional(this.state.article.property);
}
},
render() {
return <ArticleDetails article={this.state.article} />;
}
});
What's your input?
Consider move the second call to get a detail article to the ArticleDetails component componentDidMount() life cycle method.
So if the article is not set, do not render the ArticleDetails component at all by return null / false.

Categories