I'm making a small SNS app using React. (Gatsby.js)
What I want to do is to persist state in previous pages even when you go back with the browser. Like twitter or instagram, when you go to follow or follower page and visit more pages, you do not lose data when you go back. I can't find a single clue.
Is it something to do with history api or routing?
There is also a User page and it contains some links to following/follower pages in my app. When a user reaches on a page, I am fetching API in useEffect hook using url params then store it to global state(recoil), show on the component.
Here is a problem.
When I visit user page and move forward to follower page, then visit another user page from there and move to his follower page, when I go back with the browser, they don't remember the global state (of course) and it'll get fetch data again which shows loading. Tried to clean data when unmounting because it shows previous data when you go to other page. Couldn't find the bast practice.
Probably it's nothing to do with global states (Recoil), somehow window is remembering what was in the previous pages (and scroll position too)?
I'd appreciate any advice. Thank you.
React/GatsbyJS
Router/Reach-router
Route
...
<PrivateRoute path="/user/:userId" component={UserPage} />
<PrivateRoute path="/:userId/follower" component={FollowerPage} />
...
UserPage
const UserPage = () => {
const { userId } = useParams()
const user = useRecoilValue(userProfileViewData)
const loading = useRecoilValue(loadingUserPage)
...
useEffect(() => {
... // fetch api to get user info
...// store it in global state -> userProfileViewData
}, [])
if(loading) return <div>Loading</div>
return (
<div>
<div>{user.name}</div>
...
<div onClick={() => navigate('/app/' + userId + '/follower')}>Follower</div>
...
</div>
)
}
export default UserPage
FollowerPage
const FollowerPage = () => {
const { userId } = useParams()
const [loading, setLoading] = useState(true)
const followerData = useRecoilValue(followers)
...
useEffect(() => {
...// api calls to get followers
...// store it in global state -> followers
}, [])
useEffect(() => {
return () => {
.. // I have a function to clean data here
}
}, [])
if(loading) {
return <div>loading....</div>
}
return (
<div>
<div>Follower</div>
<div>
{followerData.map(user => (
<div key={`user.id}>
<div onClick={() => navigate(`/app/user/` + user.id)}>
{user.name}
</div>
</div>
))}
</div>
</div>
)
}
export default FollowerPage
Maybe you can use Redux to globally remember state of values
Related
I am building a Simple React App and I am using api to set state and I am using Link to get into detail page But When I go into another page from Link and press back button then It is setting state again. But I am trying to prevent setting state again when I go back to the page from back button.
App.js
function MainPage() {
const [blogs, setBlogs] = useStae([]);
useEffect(() => {
axios.get("/api/blogs/").then((res) => {
setBlogs(res.data.blogs);
}
}
return (
<>
{
blogs.map((res) => <>
<b>{res.title}
// It is showing about 40 blogs.
<Link to={`blog-detail/${res.id}`}>Blog Detail</Link>4
</>
}
</>
)
}
When I click on Blog Detail and press button in Browser (Chrome) then It is setting state again.
What I have tried ?
I have also thought about Using LocalStorage with Redux Store like :-
const saveToLocalStorage = (state) => {
try {
localStorage.setItem('state', JSON.stringify(state));
} catch (e) {
console.error(e);
}
};
But the Array was 40 in length then I thought Would it be efficient to store that big array (40 maybe 100(max)) in localStorage ?`
Then I cancelled it.
Using only localStorage without redux but It was localStorage after all.
I have tried many times but it is still setting state with makeing request to backend server.
Any help would be much Appreicated.
If you want to keep the MainPage component from running the effect each time it mounts then you'll need to Lift State Up to a common parent/ancestor component that remains mounted while the app's router is matching and rendering different routed content/pages/components.
You can move the blogs state up to the component rendering the routes so the data is fetched only once when mounted and lives for the duration of the component's life. Don't forget to include a dependency array for the useEffect hook!
Example:
const App = () => {
const [blogs, setBlogs] = useState([]);
useEffect(() => {
axios.get("/api/blogs/")
.then((res) => {
setBlogs(res.data.blogs);
});
}, []); // <-- empty dependency array, run effect once on mount
return (
<Routes>
<Route
path="/blog"
element={<MainPage blogs={blogs} />} // <-- pass blogs as prop
/>
<Route
path="/blog-detail/:id"
element={<BlogDetails blogs={blogs} />} // <-- pass blogs as prop
/>
</Routes>
);
};
...
function MainPage({ blogs }) { // <-- consume blogs as prop
return (
<>
{blogs.map((blog) => (
<React.Fragment key={blog.id}>
<b>{blog.title}</b>
// It is showing about 40 blogs.
<Link to={`blog-detail/${blog.id}`}>Blog Detail</Link>
</React.Fragment>
)}
</>
)
}
Switching/navigating between routes now will no longer trigger the useEffect hook to refetch the blogs data.
Problem
I'm trying to make an SPA with routing (ideally with React hooks) in React, but all the examples, descriptions i find are about displaying different components based on the URL. What i want is something like Youtube or Google docs, where the page structure/components are (mostly) the same and only the content changes.
Context
(Edit: adding a bit more context.)
This is going to be a document editor/presenter.
Page structure: after login, there is always a toolbar(blue color) on the top, for menus, notifications, etc. The rest of the screen will be mostly like the two examples below:
Example1:
Example2:
The search pane(orange) could be switched on/off by a button on the toolbar or by a user session variable. The document will be presented in the document section(grey) based on either a user session variable, doc ID provided in URL or selecting a document in the search pane.
Planned URLs
(Added in edit.)
Landing page: /login , login page.
Landing page: / , here the toolbar and a preconfigured, user session based default doc would be presented.
Document page: /doc?id=oys2OPkfOwQ , same as landing page but document section would contain the document with ID provided as query param.
Anything else: /something , toolbar and something under it.
Idea
(Added in edit.)
The layout is defined by CSS grid and page structure changes based on a variable. So this is going to be a prop for the App component coming from default value and user session configured variable and could change later.
This is the functionality i imagine for the App component (pseudo code-like thing):
<Router>
<Route path='/login'>
<Login/>
// Components: Toolbar and something under it
</Route>
<Route path='/'>
<DocApp/>
// Components: Toolbar, Document or Toolbar, Search and Document
// Default document loaded for default, not logged in user
// Default document loaded from stored user session
</Route>
<Route path='/doc'>
<DocApp/>
// Components: Toolbar, Document or Toolbar, Search and Document
// Same as for '/' except document with ID set as query param is displayed
// This could be called/triggered from search and document component as well
</Route>
<Route path='/somethingelse'>
<SomethingElse/>
</Route>
</Router>
Question
(Edit: rephrased, original question was how to implement a solution where different documents loaded based on URL query parameter.)
What i'm mostly interested in if there is a simpler way to draw the landing layout '/' and specific doc presenter /doc?id=oys2OPkfOwQ layout? In both cases the same components get displayed, only the provided parameter(doc to present) is different.
Solution
(Added in edit.)
By reading the answers and feedback and re-thinking my problem i realized that i have a multiple URLs same content problem.
Using React Router to render components based on UrlParams.
First of all, edit your routes to render DocumentLoader component under the route /doc
// file: app.js
import React from "react";
import { BrowserRouter, Route } from "react-router-dom";
import DocumentLoader from "./DocumentLoader";
const App = (props) => {
return <BrowserRouter>
<Routes>
<Route path="/doc" element={<DocumentLoader />}>
</Routes>
</BrowserRouter>
}
Create custom hooks for loading documents
You need two custom hooks, one for loading new document by changing the docId query parameter, and another hook to listen to docId changes to reload new document from your backend.
NOTE: Edit loadDocumentData to load from your backend
// file: hooks.js
import { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
/**
* TODO:// Refactor this function to call your backend to get
* Document data by docId
*/
const loadDocumentData = (docId) =>
new Promise((resolve, reject) => {
// this setTimeout for demonstration porpuse only
setTimeout(() => {
resolve({ id: docId, name: `Document name for ${docId}` });
}, 3000);
});
export const useDocument = () => {
const [loading, setLoading] = useState(true);
const { docId, loadDocument } = useDocumentParam();
const [document, setDocument] = useState(null);
useEffect(() => {
setLoading(true);
// Load your document data based on docID
loadDocumentData(docId)
.then((doc) => {
setDocument(doc);
setLoading(false);
})
.catch((e) => {
console.error('Failed to load doc', docId);
});
}, [docId, setLoading]);
return { document, loading, loadDocument };
};
export const useDocumentParam = () => {
const [searchParams, setSearchParams] = useSearchParams();
const docId = searchParams.get('d');
const loadDocument = useCallback(
(newDocId) => {
setSearchParams({ d: newDocId });
},
[setSearchParams]
);
return { docId, loadDocument };
};
Create DocumentLoader component
To listen on query param changes, load document from server-side, display loading indicator and render the "DocPresenter" component.
// file: DocumentLoader.js
import * as React from 'react';
import DocPresenter from './DocPresenter';
import { useDocument } from './hooks';
const DocumentLoader = (props) => {
const { loading, document, loadDocument } = useDocument();
if (loading) {
return <div>Display loading indicator while loading the document</div>;
}
return (
<div className="document-container">
<div className="toolbar">NavBar</div>
<div className="searchbox">search component</div>
<div className="editor">
<DocPresenter document={document} setParentstate={loadDocument} />
</div>
</div>
);
};
export default DocumentLoader;
Checkout Live Example on StackBlitz.
Helper Links:
React Router Docs
React Custom Hooks Docs
Here's how I would do it. Notice that the URL will remain the same.
const DynamicComponent = () => {
const components = {
Component1: <Component1 />,
Component2: <Component2 />,
Component3: <Component3 />,
};
const [component, setComponent] = useState(components["Component1"]);
return (
<div>
<div id="nav">
<span onClick={(e) => setComponent(components["Component1"])}>
Set to component 1
</span>
<span onClick={(e) => setComponent(components["Component2"])}>
Set to component 2
</span>
<span onClick={(e) => setComponent(components["Component3"])}>
Set to component 3
</span>
</div>
<div>{component}</div>
</div>
);
};
export default DynamicComponent;
I am making a web application that fetches data from API using axios, and showes the data.
However, fetching API takes too long, so I used Loader component, and after the axios GET method is over, I wanted to show the fetched data.
But my problem is, it fetches with no errors, Loader works perfect, and after fetching is done, Loader goes off. But my mapped data does not render on web. How can I fix this problem?
const [isLoading, setLoading] = useState(true);
const [lunchData, setLunchData] = useState(mockServingData);
useEffect(() => {
axios.get(URL).then((response) => {
setLunchData(response.data.menu[0].lunch);
lunch = lunchData.lunch;
setLoading(false);
});
}, []);
if (isLoading) {
return <div className="App"><Loader type="spin" color="RGB 값" message="Loading..." /></div>;
}
const lunchMenuList = lunch.map((menu, index) => (
<li key={index}>{menu}</li>
));
return (
<div className="App">
{lunchMenuList}
</div>
);
I used var lunch just in development process, because mockServingData is not in the perfect form.
I want the logout button to show up only when the user's logged in. It kind of works now but it's not working the way it should.
With the code below, the logout button shows up when the user's logged in only if the page's refreshed. That's because upon the <App/> component being loaded, the <Navbar/> component mounted along with it.
But how can I make it so that even if <Navbar/>'s loaded, it can still be possible to manipulate when the button can appear based on if the auth token is not null?
Here's App.js:
const App = () => {
let [logoutButtonFlag, setLogoutButtonFlag] = useState(false);
let authToken = localStorage.getItem('token');
useEffect(() => {
if (authToken !== null) {
setLogoutButtonFlag(true);
}
}, [authToken]);
return (
<>
<Navbar logoutButtonFlag={logoutButtonFlag}/>
</>
);
}
export default App;
Here's Navbar.js:
const Navbar = (props) => {
return (
{!props.logoutButtonFlag ? null : <button className="learn-more">Logout</button>}
);
};
export default Navbar;
you are providing a non-state variable to the list of states that useEffect hook 'listen' to, so it will not run again after you change its value.
const [authToken, setAuthToken] = useState(localStorage.getItem('token'));
and when you update the local storge "token" also update authToken to the same value.
and your useEffect will retrigger on authToken change because now its a state
useEffect(() => {
if (authToken !== null) {
setLogoutButtonFlag(true);
}
}, [authToken]);
The reason way only on refresh it was updating is because the value of the "token" in local storage was changed already.
I have the following situation: In component <Home /> I have a component made my me, <AutocompleteSearch /> which displays a list of books, and when we click one book, it does an api call to fetch the details about clicked book then and dispatch that saves in the books state, the current book. My problem is that I have an listener in the <Home /> component that when is triggered it checks if some value is equal with the current book id, and for that I use useSelector to get the current book in the <Home />, but I get an error saying that currentBook is undefined, I printed the books state and it prints an empty object that is the initial state. The strange thing is that I have another component in my <Home />, called to which I pass as props the current book, and in BookProfile the current book prop is a fine object, fetched from server, so the state updates correct, but when I try to access the current book in the <Home /> it look like it never changed, and as well I wanna say, I tried to use the same useSelector in another child component of home, and it works here too, it logs in console a fine object from server, representing the last clicked book in the AutocompleteSearch.
I'll insert some code snippets:
const currentUser = useSelector(state => state.users.currentUser)
const currentBook = useSelector(state => state.book.currentBook)
return (
...........
{openSearchBar && (
<AutoCompleteSearch
suggestionsList={bookListForSearchBar}
elementClickAction={setMainContent}
/>
)}
...........
<BookProfile
book={currentBook}
user={currentUser}
/>
...
}
const bookListClicked = async book=> {
await dispatch(getCurrentBook(book.id))
}
const currentBookReceived = book=> ({
type: actions.CURRENT_BOOK_RECEIVED,
anime,
})
export default function News() {
const currentUser = useSelector(state => state.users.currentUser)
const currentBook = useSelector(state => state.book.currentBook)
return (
<>
<div
onClick={() => {
console.log(currentUser)
console.log(currentBook)
}}>
News
</div>
</>
)
}
In News components state works fine, it logs the correct book, but in Home it logs undefined.
let initialState={}
export default (state = initialState, action) => {
switch (action.type) {
case actions.CURRENT_BOOK_RECEIVED:
return { ...state, currentBook: action.book }
default:
return state
}
}
Also, currentUser is fetched from server after succes at login then saved in users state with dispatch, and I printed it in console in Home and it is as it should be, not undefined like currentBook.
I am new in front-end development and I don't understand very well how the things works, I don't understand why in one component the state is seen well and in another it is not.