Combining useEffect, useContext, and/or useSWR to minimise renders on static pages with dynamic content - react-hooks

I suspect I'm overcomplicating things, and there's a much simpler solution here, but I'm new to NextJS ... and am clearly doing it wrong.
I'm generating contest landing pages from a headless CMS using NextJS ("the contest page"). Any given user might enter multiple contests, and go to any one/many of the contest pages.
The requirement is, the contest page displays how many "points" the user has in the contest, and there are components where if the user takes certain actions (e.g., clicking on them) he gets more points.
Which buttons are displayed and how many points each is worth are defined in the CMS.
So, my current thinking (which isn't working) is:
a tuple of [userId, contestId] is stored in localStorage for each contest entered (they enter by submitting a form on a different landing page, which redirects to one of these that I'm currently building)
the contest page is wrapped in a context provider, so the action components know to which [userId, contestId] they should be adding points in the database; and so the display component knows the current point value
SWR queries for the point value, so it can do its magic of "fast/lightweight/realtime"
Where I currently am:
// /pages/contests/[contest].js
export default function ContestEntryPage({ data }) {
return (
<ContestProvider>
<PageLandingPage data={data} />
</ContestProvider>
);
}
And then the landing page component:
// /components/PageLandingPage.js
const fetcher = async ({pageId, contestId, clientId}) => {
const res = await fetch({
pathname: '/api/getpoints',
query: {
pageId,
contestId,
clientId
}
});
return res.json();
}
const PageLandingPage = ({ data }) => {
const { dispatchContest } = useContest();
let registration, points;
if (data?.contestPage?.id) {
registration = // complicated parsing to extract the right contest from localStorage
const { data: fetchedData, error } = useSWR({
pageId: data.contestPage.id,
contestId: registration.contestId,
clientId: registration.clientId
}, fetcher);
points = fetchedData;
}
useEffect(() => {
// Don't waste the time if we're not a contest page
if (!data?.contestPage?.id) return;
dispatchContest({
payload: {
contestId: registration.contestId,
clientId: registration.clientId,
points: points
},
type: 'update'
})
}, [data, points, registration, dispatchContest])
return (
<div>
Awesome content from the CMS
</div>
)
}
export default PageLandingPage
As written above, it gets stuck in an infinite re-rendering loop. Previously, I had the SWR data fetching inside of the useEffect, but then it complained about calling a hook inside of useEffect:
const PageLandingPage = ({ data }) => {
const { dispatchContest } = useContest();
useEffect(() => {
// Don't waste the time if we're not a contest page
if (!data?.contestPage?.id) return;
const registration = // get from localStorage
const { data: points, error } = useSWR({
pageId: data.contestPage.id,
contestId: registration.contestId,
clientId: registration.clientId
}, fetcher);
dispatchContest({
payload: {
contestId: registration.contestId,
clientId: registration.clientId,
points: points
},
type: 'update'
})
}, [data, dispatchContest])
Obviously, I haven't even gotten to the point of actually displaying the data or updating it from sub-components.
What's the right way to combine useEffect, useContext, and/or useSWR to achieve the desired outcome?
Though I suspect it's not relevant, for completeness' sake, here's the useContext code:
import { createContext, useContext, useReducer } from 'react';
const initialState = {
contestId: null,
clientId: null
};
const ContestContext = createContext(initialState);
function ContestProvider({ children }) {
const [contestState, dispatchContest] = useReducer((contestState, action) => {
return {
...contestState,
...action.payload
}
}, initialState);
return (
<ContestContext.Provider value={{ contestState, dispatchContest }}>
{children}
</ContestContext.Provider>
);
}
function useContest() {
const context = useContext(ContestContext);
if (context === undefined) {
throw new Error('useContest was used outside of its provider');
}
return context;
}
export { ContestProvider, useContest }

Related

why is an array fetched from backend not in the same order in redux store (react app)?

In my React app, i am fetching an array of posts from a backend api (nodejs/SQL DB).
I am using redux for the frontend, so i thought it would be a good idea to sort the posts on the backend and send them to the frontend (sorted by id, from latest to oldest).
Then, the array of posts gets stored in my redux store.
It's working fine, but i am confused because when i check the store, the posts are not ordered anymore, or rather: the same 4 random posts always get "pushed" to the top and then the rest is ordered as i wanted.
So when i refresh the page i can see these older random posts in the UI at the top of the thread/feed of posts and when component is fully mounted it renders posts in the correct order. Not good.
I wanted to avoid sorting the array of posts on the frontend for performance concerns, am i wrong?
Redux initial state:
const initialState = {
posts: [],
userPosts: [],
currentPost: {
title: "",
text: "",
imgUrl: "",
},
scrapedPost: {},
comments: [],
replies: [],
likes: [],
error: "",
lastPostAdded: null,
lastReplyAdded: null,
lastDeleted: null,
sessionExpired: false,
users: [],
};
Redux root reducer:
import { combineReducers } from "redux";
import { postsReducer } from "./posts.reducer.js";
import { userReducer } from "./user.reducer.js";
export const rootReducer = combineReducers({
user: userReducer,
posts: postsReducer,
});
Redux store config:
import { applyMiddleware, createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import { persistReducer, persistStore } from "redux-persist";
import autoMergeLevel2 from "redux-persist/lib/stateReconciler/autoMergeLevel2";
import storage from "redux-persist/lib/storage";
import thunk from "redux-thunk";
import { rootReducer } from "./reducers/root.reducer";
const composeEnhancer = composeWithDevTools({ trace: true, traceLimit: 25 });
const persistConfig = {
key: "root",
storage,
stateReconciler: autoMergeLevel2,
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = createStore(persistedReducer, composeEnhancer(applyMiddleware(thunk)));
const persistor = persistStore(store);
export { store, persistor };
getPost action creator (using thunk middleware for async task):
export const getPosts = () => async (dispatch) => {
const accessToken = localStorage.getItem("jwt");
const request = {
headers: {
"Access-Control-Allow-Origin": "*",
Authorization: `Bearer ${accessToken}`,
},
method: "get",
};
try {
const response = await fetch(API_POST, request);
const data = await response.json();
const { posts, likes, sessionExpired } = data;
if (sessionExpired) {
dispatch({ type: SESSION_EXPIRED, payload: sessionExpired });
return;
}
dispatch({ type: GET_POSTS, payload: { posts, likes } });
} catch (error) {
dispatch({ type: SET_ERROR_POST, payload: error.message });
}
}
the posts reducer:
export const postsReducer = (state = initialState, action) => {
switch (action.type) {
case GET_POSTS: {
const { posts, likes } = action.payload;
return { ...state, posts, likes };
}
case GET_LIKES: {
const { likes } = action.payload;
return { ...state, likes };
// all other actions...//
}
relevant part of the UI code (feed component):
const Feed = () => {
const [newUser, setNewUser] = useState(false);
const user = useSelector((state) => state.user);
const { isAuthenticated, isNewUser } = useSelector((state) => state.user);
const posts = useSelector((state) => state.posts.posts);
const dispatch = useDispatch();
const userLanguage = useLanguage();
useEffect(() => {
window.scrollTo(0, 0);
setNewUser(isNewUser);
return function cleanup() {
setNewUser(null);
};
}, [isNewUser]);
useEffect(() => {
dispatch(getPosts());
}, []);
return (
<Layout>
//some jsx...//
<button className="h-6 refreshBtn outline-none hover:cursor-pointer bg-blue-500
text-white rounded-full gap-1 flex items-center justify-center pl-2 pr-3 py-1
shadow transition-all duration-300 hover:bg-black hover:shadow-none group"
onClick={() => dispatch(getPosts())}
style={{ opacity: posts && posts.length !== 0 ? 1 : 0 }}>
<RefreshIcon className="h-4 w-4 pointer-events-auto transform transition
transform duration-500 group-hover:-rotate-180" />
<span className="text-xs pointer-events-auto capitalize">
{userLanguage?.feed.refreshBtn}</span>
</button>
<div className="posts-wrapper h-full w-full relative flex flex-col items-center
justify-center gap-4 pb-6">
{posts.length === 0
? (<Skeleton element="post" number={8} />)
: (posts.map((post) => <Post key={post.postId} post={post} />)}
</div>
</Layout>
};
posts ordered by Id on the backend:
screenshot
posts in the redux store (as you can see by their postId, indexes 0 to 3 have nothing to do there)
screenshot
so my questions:
how come the array fetched is not in the same order in redux store?
why does the UI flash the "wrong" order for a sec, then the correct order? how does it know the correct order if those 4 posts are still at the top in the store?
i'm confused here, any hint or help is appreciated! thanks
I finally found the solution months ago but forgot to come back here to give the solution to the issue i had.
Turns out the order of the posts fetched from backend wasn't modified or messed up with by Redux at all but by me (of course!) from another component called PopularPosts.
Consider the code below:
const PopularPosts = () => {
const { posts } = useSelector(state => state.posts);
const [top3, setTop3] = useState<IPost[]>([]);
useEffect(() => {
setTop3(posts.sort((a, b) => { my sorting logic }).splice(0, 3));
}, [posts]);
I was literally mutating the store directly in order to create my top3. Of course this was a HUGE mistake! I should have used the sort() method on a COPY of the store, not the store itself.
Here is the correct code:
const PopularPosts = () => {
const { posts } = useSelector(state => state.posts);
const [top3, setTop3] = useState<IPost[]>([]);
useEffect(() => {
const postsCopy = [...posts];
setTop3(postsCopy.sort((a, b) => { // my sorting logic }).splice(0, 3));
}, [posts]);
All is working as intended since this correction.
And lesson learnt: i'll never mutate the Redux store directly ever again ;)

Redux connected React component not updating until a GET api request is recalled

My react app uses a redux connected component to render data from backend for a project page, so I called a GET dispatch inside a React Hook useEffect to make sure data is always rendered when the project page first open, and whenever there is a change in state project, the component will be updated accordingly using connect redux function. However, the component doesn't update after I reduce the new state using a DELETE API request, only if I dispatch another GET request then the state will be updated. So I have to call 2 dispatches, one for DELETE and one for GET to get the page updated synchronously (as you can see in handleDeleteUpdate function), and the same thing happened when I dispatch a POST request to add an update (in handleProjectUpdate). Only when I reload the page, the newly changed data will show up otherwise it doesn't happen synchronously, anyone knows what's wrong with the state update in my code? and how can I fix this so the page can be loaded faster with only one request?
I've changed the reducer to make sure the state is not mutated and is updated correctly.
I have also tried using async function in handleDeleteUpdate to make sure the action dispatch is finished
I have tried
console.log(props.project.data.updates)
to print out the updates list after calling props.deleteUpdate but it seems the updates list in the state have never been changed, but when I reload the page, the new updates list is shown up
Here is the code I have for the main connected redux component, actions, and reducers file for the component
function Project(props) {
let options = {year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit'}
const {projectID} = useParams();
const history = useHistory();
console.log(props.project.data? props.project.data.updates : null);
console.log(props.project.data);
// const [updates, setUpdates] = useState(props.project.data? props.project.data.updates : null)
useEffect(() => {
props.getProject(projectID);
}, []);
// Add an update to project is handled here
const handleProjectUpdate = async (updateInfo) => {
await props.postProjectUpdate(projectID, updateInfo)
await props.getProject(projectID);
}
const handleDeleteUpdate = async (updateID) => {
await props.deleteUpdate(projectID, updateID);
await props.getProject(projectID);
console.log(props.project.data.updates);
};
return (
<div>
<Navbar selected='projects'/>
<div className = "project-info-layout">
<UpdateCard
updates = {props.project.data.updates}
handleProjectUpdate = {handleProjectUpdate}
handleDeleteUpdate = {handleDeleteUpdate}
options = {options}
/>
</div>
</div>
)
}
const mapStateToProps = state => ({
project: state.project.project,
});
export default connect(
mapStateToProps,
{getProject, postProjectUpdate, deleteUpdate}
)(Project);
ACTION
import axios from 'axios';
import { GET_PROJECT_SUCCESS,ADD_PROJECT_UPDATE_SUCCESS, DELETE_PROJECT_UPDATE_SUCCESS} from './types';
let token = localStorage.getItem("token");
const config = {
headers: {
Authorization: `Token ${token}`,
}
};
export const getProject = (slug) => dispatch => {
axios.get(`${backend}/api/projects/` + slug, config)
.then(
res => {
dispatch({
type: GET_PROJECT_SUCCESS,
payload: res.data,
});
},
).catch(err => console.log(err));
}
export const postProjectUpdate = (slug, updateData) => dispatch => {
axios.post(`${backend}/api/projects/`+slug+ `/updates`,updateData, config)
.then(
res => {
dispatch({
type: ADD_PROJECT_UPDATE_SUCCESS,
payload: res.data,
});
},
).catch(err => console.log(err));
}
export const deleteUpdate = (slug, updateID) => dispatch => {
axios.delete(`${backend}/api/projects/`+ slug + `/updates/`+ updateID, config)
.then(
res => {
dispatch({
type: DELETE_PROJECT_UPDATE_SUCCESS,
payload: updateID,
});
},
).catch(err => console.log(err));
}
Reducer
import { GET_PROJECT_SUCCESS,ADD_PROJECT_UPDATE_SUCCESS, DELETE_PROJECT_UPDATE_SUCCESS} from "../actions/types";
const initialState = {
project: {},
};
export default function ProjectReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_PROJECT_SUCCESS:
return {
...state, // return all initial state
project: payload
};
case ADD_PROJECT_UPDATE_SUCCESS:
return {
...state,
project: {
...state.project,
updates: [...state.project.data.updates, payload.data]
}
};
case DELETE_PROJECT_UPDATE_SUCCESS:
let newUpdatesArray = [...state.project.updates]
newUpdatesArray.filter(update => update.uuid !== payload)
return {
...state,
project: {
...state.project,
members: newUpdatesArray
}
};
default:
return state;
}
}
updateCard in the Project component is showing a list of all updates

Google address autocomplete api in Stenciljs

I am trying to add a search field for an address using google's address autocomplete in a Stenciljs component. There aren't any resources on it.
First you'll need to load the google maps api script, so that you can interact with the global google.maps object. You can either do that by including a script tag, or write something like the following helper function.
const googleApiKey = '...';
export const importMapsApi = async () =>
new Promise<typeof google.maps>((resolve, reject) => {
if ('google' in window) {
return resolve(google.maps);
}
const script = document.createElement('script');
script.onload = () => resolve(google.maps);
script.onerror = reject;
script.src = `https://maps.googleapis.com/maps/api/js?key=${googleApiKey}&libraries=places`;
document.body.appendChild(script);
});
In order to get the TypeScript types for the global google object, you should install #types/googlemaps into your dev-dependencies.
Then you'll need to implement a function that allows you to search for places, e. g.:
export const searchForPlaces = async (input: string, sessionToken: google.maps.places.AutocompleteSessionToken) => {
const maps = await importMapsApi();
const service = new maps.places.AutocompleteService();
return new Promise<google.maps.places.AutocompletePrediction[]>((resolve) =>
service.getPlacePredictions({ input, sessionToken }, (predictions, status) => {
if (status !== maps.places.PlacesServiceStatus.OK) {
return resolve([]);
}
resolve(predictions);
}),
);
};
None of this is specific to Stencil btw. All that is left to do is to use the searchForPlaces function in your component. A very simple example would be something like:
#Component({ tag: 'maps-place-search' })
export class MapsPlaceSearch {
sessionToken: string;
#State()
predictions: google.maps.places.AutocompletePrediction[];
async componentWillLoad() {
const maps = await importMapsApi();
this.sessionToken = new maps.places.AutoCompleteSessionToken();
}
async search = (e: InputEvent) => {
const searchTerm = e.target.value;
if (!searchTerm) {
this.predictions = [];
return;
}
this.predictions = await searchForPlaces(searchTerm, this.sessionToken);
}
render() {
return (
<Fragment>
<input onInput={this.search} />
<ul>
{this.predictions.map(prediction => <li key={prediction.description}>{prediction.description}</li>)}
</ul>
<Fragment>
);
}
}
The place search will give you a placeId for each prediction. That and the session token you can pass on to a maps.places.PlacesService to get the details for the place and auto-fill your form or whatever you're trying to achieve.

Issue while updating store from updater function of commitMutation

I have a mutation
mutation createQuoteLineMutation {
createQuoteLine {
quoteLine {
name
price
product {
name
}
}
}
}
My updater function is as below.
updater: (store) => {
const payload = store.getRootField('createQuoteLine');
const newQuoteLine = payload.getLinkedRecord('quoteLine');
const quote = store.getRoot().getLinkedRecord('getQuote');
const quoteLines = quote.getLinkedRecords('quoteLines') || [];
const newQuoteLines = [...quoteLines, newQuoteLine];
quote.setLinkedRecords(newQuoteLines, 'quoteLines');
}
This works fine for the first time, but the consequent mutations all the previously added quoteLines change to new one I'm assuming this is because newQuoteLine points to same object all the time.
adding below line at the end of updater function unlink quoteLine from createQuoteLine also does not work.
payload.setValue(null, 'quoteLine');
Any help in this regard is highly appreciated.
I have seen a quite similar problem, but I am not sure if it's the same. Try to pass an clientMutationId to the mutation, and increment it along.
const commit = (
input,
onCompleted: (response) => void,
) => {
const variables = {
input: {
...input,
clientMutationId: temp++,
},
};
commitMutation(Environment, {
mutation,
variables,
onCompleted,
onError: null,
updater: store => {
// ....
},
});
};
Try something like this and let me know if it fixes :).

React Apollo subscription bypasses the graphql wrapper

I have a sample app called GraphQL Bookstore that creates books, publishers and authors and shows relationships between them. I am using subscriptions to show updates in real time.
For some reason my BOOK_ADDED subscription is bypassing the graphql wrapper completely. It is calling the wrapped class with the books prop set to undefined. Relevant parts of the code are shown below (you can see the full code here).
class BooksContainerBase extends React.Component {
componentWillMount() {
const { subscribeToMore } = this.props;
subscribeToMore({
document: BOOK_ADDED,
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) {
return prev;
}
const newBook = subscriptionData.data.bookAdded;
// Don't double add the book
if (!prev.books.find(book => book.id === newBook.id)) {
return Object.assign({}, prev, {
books: [...prev.books, newBook]
});
} else {
return prev;
}
}
});
}
render() {
const { books } = this.props;
return <BooksView books={books} />;
}
}
...
export const BooksContainer = graphql(BOOKS_QUERY, {
props: ({ data: { loading, error, subscribeToMore, books } }) => ({
loading,
error,
subscribeToMore,
books
})
})(LoadingStateViewer(BooksContainerBase));
Basically when a subscription notification is received by the client, the updateQuery() function is called - as expected. However, as soon as that function exits, the render() method of the wrapped class is called directly with books set to undefined. I expected that the graphql wrapper would be called, setting the props correctly before calling the render() method. What am I missing?
Thanks in advance!

Resources