I am trying to render the profile page using data I get from firestore based on URL content.
For example when a user types mywebsite/profile/someusername I would like to retrieve profile info from firestore database and render the Profile component. I have a redux store that keeps the state of the App and I have postReducer and authReducer and respective actions. When I use componentDidMount() lifecycle method for the first render when the users go their own profile page it shows info about them and their own data. But upon typing someone else's username on the URL I get errors like cannot read property 'props' of undefined. What would be a good strategy for me to go with? Any help would be appreciated.
My code for Profile component:
class Profile extends Component {
//component state and render method are removed to make some space
componentDidMount() {
this.props.getUserPosts(this.props.profile.username);
}
}
const mapStateToProps = (state) => {
return {
auth: state.firebase.auth,
profile: state.auth.profile,
profileError: state.auth.profileError,
userPosts: state.post.userPosts,
userError: state.post.userError
}
};
const mapDispatchToProps = (dispatch) => {
return {
getUserPosts: (username) => {
dispatch(getUserPosts(username));
},
getProfile: (username) => {
dispatch(getProfile(username));
}
}
};
export default connect(mapStateToProps, mapDispatchToProps)(Profile);
It very much depends on how you setup firebase in your project, but you would just access firestore according to their documentation. I have written a small (as small as it could be I belive) example to another question: Firebase listener with React Hooks
You are more than welcome to grap some inspiration from it.
Related
I have a form composed of several input components. The form data is shared and shareable across these sibling components via a React context and the React hook useContext.
I am stumbling on how to optionally async load data into the same context. For example, if the browser URL is example.com/form, then the form can load with the default values provided by a FormContext (no problem). But if the user is returning to finish a previously-edited form by navigating to example.com/form/:username/:form-id, then application should fetch the data using those two data points. Presumably this must happen within the FormContext somehow, in order to override the default empty form initial value.
Are url params even available to a React.createContext function?
If so, how to handle the optional data fetch requirement when hooks are not to be used with in a conditional?
How to ensure that the data fetch occurs only once?
Lastly, the form also saves current state to local storage in order to persist values on refresh. If the data fetch is implemented, should a refresh load the async data or the local storage? I'm leaning towards local storage because that is more likely to represent what the user last experienced. I'm open to opinions and thoughts on implementation.
FormContext
export const FormContext = React.createContext();
export const FormProvider = props => {
const defaultFormValues = {
firstName: "",
lastName: "",
whatever: "",
};
const [form, setForm] = useLocalStorage(
"form",
defaultFormValues
);
return (
<FormContext.Provider value={{ form, setForm }}>
{props.children}
</FormContext.Provider>
);
};
Reference for useLocalStorage
I think the answer you're looking for is Redux, not the library but the workflow. I did find it curious React doesn't give more guidance on this. I'm not sure what others are doing but this is what I came up with.
First I make sure the dispatch from useReducer is added to the context. This is the interface for that:
export interface IContextWithDispatch<T> {
context: T;
dispatch: Dispatch<IAction>;
}
Then given this context:
export interface IUserContext {
username: string;
email: string;
password: string;
isLoggedIn: boolean;
}
I can do this:
export const UserContext = createContext<IContextWithDispatch<IUserContext>>({
context: initialUserContext,
dispatch: () => {
return initialUserContext;
},
});
In my top level component I memoize the context because I only want one instance. This is how I put it all together
import memoize from 'lodash/memoize';
import {
IAction,
IContextWithDispatch,
initialUserContext,
IUserContext,
} from './domain';
const getCtx = memoize(
([context, dispatch]: [IUserContext, React.Dispatch<IAction>]) =>
({ context, dispatch } as IContextWithDispatch<IUserContext>),
);
const UserProvider = ({ children }) => {
const userContext = getCtx(useReducer(userReducer, initialUserContext)) as IContextWithDispatch<
IUserContext
>;
useEffect(() => {
// api call to fetch user info
}, []);
return <UserContext.Provider value={userContext}>{children}</UserContext.Provider>;
};
Your userReducer will be responding to all dispatch calls and can make API calls or call another service to do that etc... The reducer handles all changes to the context.
A simple reducer could look like this:
export default (user, action) => {
switch (action.type) {
case 'SAVE_USER':
return {
...user,
isLoggedIn: true,
data: action.payload,
}
case 'LOGOUT':
return {
...user,
isLoggedIn: false,
data: {},
}
default:
return user
}
}
In my components I can now do this:
const { context, dispatch } = useContext<IContextWithDispatch<IUserContext>>(UserContext);
where UserContext gets imported from the export defined above.
In your case, if your route example.com/form/:username/:form-id doesn't have the data it needs it can dispatch an action and listen to the context for the results of that action. Your reducer can make any necessary api calls and your component doesn't need to know anything about it.
Managed to accomplish most of what I wanted. Here is a gist demonstrating the basic ideas of the final product: https://gist.github.com/kwhitejr/df3082d2a56a00b7b75365110216b395
Happy to receive feedback!
I'm building a 'Hacker News' clone, Live Example using React/Redux and can't get this final piece of functionality to work. I have my entire App.js wrapped in BrowserRouter, and I have withRouter imported into my components using window.history. I'm pushing my state into window.history.pushState(getState(), null, `/${getState().searchResponse.params}`) in my API call action creator. console.log(window.history.state) shows my entire application state in the console, so it's pushing in just fine. I guess. In my main component that renders the posts, I have
componentDidMount() {
window.onpopstate = function(event) {
window.history.go(event.state);
};
}
....I also tried window.history.back() and that didn't work
what happens when I press the back button is, the URL bar updates with the correct previous URL, but after a second, the page reloads to the main index URL(homepage). Anyone know how to fix this? I can't find any real documentation(or any other questions that are general and not specific to the OP's particular problem) that makes any sense for React/Redux and where to put the onpopstate or what to do insde of the onpopstate to get this to work correctly.
EDIT: Added more code below
Action Creator:
export const searchQuery = () => async (dispatch, getState) => {
(...)
if (noquery && sort === "date") {
// DATE WITH NO QUERY
const response = await algoliaSearch.get(
`/search_by_date?tags=story&numericFilters=created_at_i>${filter}&page=${page}`
);
dispatch({ type: "FETCH_POSTS", payload: response.data });
}
(...)
window.history.pushState(
getState(),
null,
`/${getState().searchResponse.params}`
);
console.log(window.history.state);
};
^^^ This logs all of my Redux state correctly to the console through window.history.state so I assume I'm implementing window.history.pushState() correctly.
PostList Component:
class PostList extends React.Component {
componentDidMount() {
window.onpopstate = () => {
window.history.back();
};
}
(...)
}
I tried changing window.history.back() to this.props.history.goBack() and didn't work. Does my code make sense? Am I fundamentally misunderstanding the History API?
withRouter HOC gives you history as a prop inside your component, so you don't use the one provided by the window.
You should be able to access the window.history even without using withRouter.
so it should be something like:
const { history } = this.props;
history.push() or history.goBack()
I am kind of new to react and I am developing a Frontend for a REST-API.
My Frontend for a REST-API is organized in 5 Sites(Components) which are routed with react-router-dom.
Every time I enter a Site, the ComponentDidLoad dispatches an action, which in turn calls the API in my reducer.
export function getData(pURL, ...pParams){
return (dispatch) => {
axios.get(pURL, {pParams})
.then(result => {
dispatch(getDataSuccess(result.data))
})
.catch(error => {
dispatch(getDataFailure(error))
})
}
}
One of my Main sites looks as follows
class Overview extends Component {
componentDidMount(){
this.props.getData(MyURLHere);
}
render(){
return(
<div>
{this.props.hasError ? "Error while pulling data from server " : ""}
{/* isLoading comes from Store. true = API-Call in Progress */}
{this.props.isLoading ? "Loading Data from server" : this.buildTable()}
</div>
)
}
}
let mapStateToProps = state => {
hasError: state.api.hasError,
isLoading: state.api.isLoading,
companies: state.api.fetched
}
let mapDispatchToProps = {
//getData()
}
export default connect(mapStateToProps, mapDispatchToProps)(Overview);
state.api.fetched is my store-variable I store data from every API-Call in.
this.buildTable() just maps over the data and creates a simple HTML-Table
My Problem is that I got the store-state variable isLoading set to truein my initialState.
But when I move to another site and then back to this one, it automatically grabs data from state.api.fetched and tries to this.buildTable() with it, because isLoading is not true. Which ends in a "is not a function" error, because there is still old, fetched data from another site in it.
My Solution would be to always call a function when leaving a component(site) that resets my store to it's initialState
const initialStateAPI = {
isLoading: true
}
or directly set isLoading to true, in Order to avoid my site trying to use data from old fetches.
Is there a way to do so?
I hope that I provided enough information, if not please let me know.
If you want to call the function when leaving a component you can use componentWillUnmount function. Read more about React Lifecycle methods.
I am new to Redux and setting up an app with Campaign / Products / Users along with Posts from the MERN v2.0 boilerplate.
I have setup my app to have a fetchPosts action.
My page has the following (code from bottom half)
// Actions required to provide data for this component to render in sever side.
PostListPage.need = [() => {
return fetchPosts();
}];
// Retrieve data from store as props
function mapStateToProps(state) {
return {
posts: getPosts(state)
};
}
PostListPage.contextTypes = {
router: React.PropTypes.object,
};
export default connect(mapStateToProps)(PostListPage);
Get Posts passes the state and should add posts object to the store.
I can hit the API route and see that the back-end is working and loading the JSON object as expected.
However, my app is giving me an error within the PostReducer --
// Get all posts
export const getPosts = state => state.posts.data;
The error is when I go to the route --
TypeError: Cannot read property 'data' of undefined
at getPosts (/Users/yasirhossain/Projects/showintel/client/modules/Post/PostReducer.js:31:34)
at mapStateToProps (/Users/yasirhossain/Projects/showintel/client/modules/Post/pages/PostListPage/PostListPage.js:48:12)
at Connect.configureFinalMapState (/Users/yasirhossain/Projects/showintel/node_modules/react-redux/lib/components/connect.js:155:27)
Any help is appreciated!
In the combineReducer file, I forgot to add PostReducer as alluded to by free_soul in the comments.
export default combineReducers({
app,
posts,
});
Importing PostReducer and adding it, did the trick!
I have an app that has user profiles. On the user profile there are a list of friends, and when clicking on a friend it should take you to that other user profile.
Currently, when I click to navigate to the other profile (through redux-router Link) it updates the URL but does not update the profile or render the new route.
Here is a simplified code snippet, I've taken out a lot of code for simplicity sake. There are some more layers underneath but the problem happens at the top layer in my Profile Container. If I can get the userId prop to update for ProfileSections then everything will propagate through.
class Profile extends Component {
componentWillMount() {
const { userId } = this.props.params
if (userId) { this.props.getUser(userId) }
}
render() {
return <ProfileSections userId={user.id} />
}
}
const mapStateToProps = ({ user }) => {
return { user }
}
export default connect(mapStateToProps, { getUser })(Profile);
As you can see, what happens is that I am running the getUser action on componentWillMount, which will happen only once and is the reason the route changes but the profile data does not update.
When I change it to another lifecycle hook like componentWillUpdate to run the getUser action, I get in an endless loop of requests because it will keep updating the state and then update component.
I've also tried using the onEnter hook supplied by react-router on Route component but it doesn't fire when navigating from one profile to another since it's the same route, so that won't work.
I believe I'm thinking about this in the wrong way and am looking for some guidance on how I could handle this situation of navigating from one profile to another while the data is stored in the redux store.
So I would suggest you approach this in the following way:
class Profile extends Component {
componentWillMount() {
const { userId } = this.props.params
if (userId) {
// This is the initial fetch for your first user.
this.fetchUserData(userId)
}
}
componentWillReceiveProps(nextProps) {
const { userId } = this.props.params
const { userId: nextUserId } = nextProps.params
if (nextUserId && nextUserId !== userId) {
// This will refetch if the user ID changes.
this.fetchUserData(nextUserId)
}
}
fetchUserData(userId) {
this.props.getUser(userId)
}
render() {
const { user } = this.props
return <ProfileSections userId={user.id} />
}
}
const mapStateToProps = ({ user }) => {
return { user }
}
export default connect(mapStateToProps, { getUser })(Profile);
Note that I have it set up so in the componentWillMount lifecycle method, you make the request for the initial userId. The code in the componentWillReceiveProps method checks to see if a new user ID has been received (which will happen when you navigate to a different profile) and re-fetches the data if so.
You may consider using componentDidMount and componentDidUpdate instead of componentWillMount and componentWillReceiveProps respectively for the fetchUserData calls, but it could depend on your use case.