import { Container } from 'react-bootstrap'
import { useDispatch, useSelector } from 'react-redux'
import Layout from '../../components/Layout/Layout'
const CartScreen = () => {
const cart = useSelector(state => state.cart)
const { cartItems } = cart
console.log(cartItems)
return (
<Layout>
<Container>
<div>
<h1>Shopping Cart</h1>
{cartItems && cartItems.length === 0 && <p>Your cart is empty </p>}
</div>
</Container>
</Layout>
)
}
export default CartScreen
This code gives me that Hydration Error only when 'cart' is empty
I don't know how to use useEffect on this and I use redux toolkit.
I am making a page which has many editable input fields.
I am using redux-toolkit and redux-thunk for this.
I am fetching data from api and put them into redux store. First render of component I want to reflect data in the store to the component's local state so that I can edit them and onSubmit of form, I am planning to re-put new values to store again.
Is this good approach?
Now it does not render anything and I think it because component rendering before api response arrives.
Is it possible without using redux-forms?
Component
import React, { useEffect, useState } from 'react'
import { Col, Row } from 'reactstrap'
import PreviewCaseTable from './refactor/PreviewCaseTable'
import { useDispatch, useSelector } from 'react-redux'
import { fetchCases, fetchCollections } from '../../../store/case/caseSlicer'
const TaskPreview2 = (props) => {
const dispatch = useDispatch()
const TaskStore = useSelector(store => store.Task)
const CaseStore = useSelector(store => store.Case)
const [cases, setCases] = useState(CaseStore.cases)
const [collections, setCollections] = useState(CaseStore.collections)
useEffect(() => {
dispatch(fetchCases({
taskId: TaskStore.selectedTask.taskId,
page: 0,
size: 100,
taskType: TaskStore.selectedTask.taskType
}))
dispatch(fetchCollections({ taskId: TaskStore.selectedTask.taskId }))
}, [])
return (
<div className="page-content">
<Row>
<Col>
{/*<UpdateWeightCard/>*/}
</Col>
<Col>
{/*<StrategyCard/>*/}
</Col>
</Row>
<Row>
{console.log("Cases:", cases)}
{cases.length > 0 && <PreviewCaseTable
cases={cases}
collections={collections}/>}
</Row>
</div>
)
}
I'm currently working on a site that pulls Reddit data from different subreddits to show recipes, you can toggle between 3 different ones. That part works fine! I'm able to pull the list of posts from each subreddit, but now I'm trying to make it so when you click a button, it routes you to post details where it'll show the post and comments. I have to do another API call to get that information.
Somewhere along the way, it's getting messed up and it's showing "xhr.js:210 GET https://www.reddit.com/r/recipes/comments/[object%20Object].json 404:" It's a dynamic route, so there are two different parameters I'm trying to use. I did try and console log the parameter for ID(the one showing up as [object object] and it shows up fine by itself and is not an object from what I can tell.
Please see some of the code below where I think things could be going wrong. I'm guessing it's the API call because when I console log it, it only shows the subreddit as an arg but not sure..
redditAPI.js:
import axios from 'axios';
export default axios.create({
baseURL:"https://www.reddit.com/r/"
})
store.js:
import {configureStore} from '#reduxjs/toolkit';
import postReducer from './posts/postSlice';
export const store = configureStore({
reducer: {
posts: postReducer
}
});
postSlice.js:
import {createSlice, createAsyncThunk} from '#reduxjs/toolkit';
import redditApi from '../../common/api/redditApi';
import { redditDetails } from '../../common/api/redditApi';
export const fetchAsyncPosts = createAsyncThunk('posts/fetchAsyncPosts', async (subreddit) => {
const response = await redditApi.get(subreddit)
return response.data.data.children;
});
export const fetchAsyncPostsDetail = createAsyncThunk('posts/fetchAsyncPostsDetail', async (sub, postID) => {
const response = await redditApi.get(`${sub}/comments/${postID}.json`)
return response.data.data.children;
});
const initialState = {
posts: [],
selectedSubreddit: 'recipes.json',
selectedPost: []
}
const postSlice = createSlice({
name: "posts",
initialState,
reducers: {
addPosts: (state, { payload }) => {
state.posts = payload;
},
addSelectedPost: (state, {payload}) => {
state.selectedPost = payload;
},
setSelectedSubreddit(state, action) {
state.selectedSubreddit = action.payload;
}
},
extraReducers: {
[fetchAsyncPosts.pending] : () => {
console.log("Pending");
},
[fetchAsyncPosts.fulfilled] : (state, {payload}) => {
console.log("Fulfilled");
return {...state, posts: payload};
},
[fetchAsyncPosts.rejected]: () => {
console.log("Rejected");
},
[fetchAsyncPostsDetail.fulfilled] : (state, {payload}) => {
console.log("Fulfilled");
return {...state, selectedPost: payload};
},
},
});
export const {addPosts, setSelectedSubreddit} = postSlice.actions;
export const selectSelectedSubreddit = (state) => state.selectedSubreddit;
export const getAllPosts = (state) => state.posts.posts;
export const getSelectedPost = (state) => state.posts.selectedPost;
export default postSlice.reducer;
App.js
import "./App.scss";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import PageNotFound from "./components/PageNotFound/PageNotFound";
import Header from "./components/Header/Header";
import Home from "./components/Home/Home";
import PostDetails from "./components/PostDetails/PostDetails";
import Footer from "./components/Footer/Footer";
import 'bootstrap/dist/css/bootstrap.min.css';
function App() {
return (
<div className="App">
<Router>
<Header/>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/:sub/comments/:postID" element={<PostDetails />} />
<Route path="*" element={ <PageNotFound />} />
</Routes>
</Router>
</div>
);
}
export default App;
Home.js:
import React, {useEffect} from 'react'
import { useDispatch, useSelector } from 'react-redux';
import PostListing from '../PostListing/PostListing'
import { fetchAsyncPosts } from '../../features-redux/posts/postSlice';
function Home() {
const dispatch = useDispatch();
const subreddit = useSelector((state) => state.posts.selectedSubreddit);
useEffect(() => {
dispatch(fetchAsyncPosts(subreddit));
}, [dispatch, subreddit]);
return (
<div>
<div className="jumbotron jumbotron-fluid">
<div className="container text-center">
<h1 className="display-4">Welcome to Tasteful Reddit</h1>
<p className="lead">Toggle between subreddits above to view their recipes.</p>
</div>
</div>
<PostListing />
</div>
)
}
export default Home
postListing.js:
import React from 'react'
import { useSelector } from 'react-redux'
import { getAllPosts } from '../../features-redux/posts/postSlice'
import PostCard from '../PostCard/PostCard';
function PostListing() {
const posts = useSelector(getAllPosts);
const rendering = () => posts.map((post, key) => {
return <PostCard key={key} data={post.data} />;
});
console.log(posts);
return (
<div className="post-wrapper">
<div className="post-container">
{rendering()}
</div>
</div>
)
}
export default PostListing
postCard.js:
import React from 'react'
import parse from 'html-react-parser';
import {Link} from 'react-router-dom';
function PostCard(props) {
const {data} = props;
/* Function to change escaped HTML to string */
const htmlDecode = (input) => {
var doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}
/* Decode reddit JSON's youtube embed HTML */
const youtubeHtmlString = htmlDecode(data.media_embed.content);
/* This function runs through the reddit data to make sure that there is a
an image for the post. If so, shows image
and if its a reddit hosted video or youtube video it will render the video.
Gallery-style posts & all else shows empty div*/
const mediaRender = () => {
if (data.thumbnail !== 'self' && data.thumbnail !== 'default' && data.is_self !== true && data.is_gallery !== true && data.domain !== 'youtu.be' && data.domain !== 'v.redd.it') {
return <img src = {data.url} alt={data.title} className="card-img-top"/>;
} if ( data.is_video == true) {
return (
<div>
<video controls preload = "none">
<source src={data.media.reddit_video.fallback_url} type="video/mp4"/>
Your browser does not support the video tag.
</video>
</div>
)
} if (data.domain == 'youtu.be') {
return (
<div className="Container">
{parse(youtubeHtmlString)}
</div>
)
} else {
return <div></div>
}
}
/* If only text & no photos, render text info*/
const renderSelf = () => {
if(data.is_self == true) {
return (<p>{data.selftext}</p>)
} else {
return <p></p>
}
}
return (
<div className="card mb-3 mx-auto text-center" style={{width: "70%"}}>
<div className="row g-0">
<div className="col-md-5">
{mediaRender()}
</div>
<div className="col-md-7">
<div className="card-body">
<h5 className="card-title">{parse(data.title)}</h5>
<div className="card-text">{renderSelf()}</div>
<div className="card-text"><small className="text-muted">By {data.author}</small></div>
<Link to={`/${data.subreddit}/comments/${data.id}`}>
<button className="btn btn-primary">Go to post</button>
</Link>
</div>
</div>
</div>
</div>
)
}
export default PostCard
postDetails.js:
import React, { useEffect } from 'react'
import { useParams } from 'react-router-dom';
import {useDispatch, useSelector} from 'react-redux';
import { fetchAsyncPostsDetail, getSelectedPost } from '../../features-redux/posts/postSlice';
function PostDetails() {
let {sub, postID} = useParams();
const dispatch = useDispatch();
const data = useSelector(getSelectedPost);
console.log(postID)
useEffect(() => {
dispatch(fetchAsyncPostsDetail(sub, postID));
console.log(dispatch(fetchAsyncPostsDetail(sub, postID)))
}, [dispatch, postID, sub]);
console.log(data);
return (
<div>
PostDetails
</div>
)
}
export default PostDetails
Any help would be appreciated, because I'm lost!
Thanks!
I figured it out today, for those interested!
I finally got it! I figured I was doing something wrong with react router, and that's why I couldn't get it to work but I didn't want to stop using react router. I tried many different ways to fix it, including putting both arguments in an object, I tried changing it by passing what I needed with a pathname AND a state, but the state didn't work right. think that I was passing the state in an incorrect way somehow. I think I wrote it like state{ permalink: data.permalink} or something. I also tried changing it so it only took one parameter, but for some reason the react router Link only wanted to use the post IDs and would go to my page not found if I tried to use permalink. After researching other ways to pass data using React Router, I landed on using From to grab the data from the previous page the user was on and used useLocation to grab the state.
Here is a snippet of what I changed in Link for in PostCard.js. The data was passed in props from postListing.js, just for reference:
return (
<div className="card mb-3 mx-auto text-center" style={{width: "70%"}}>
<div className="row g-0">
<div className="col-md-5">
{mediaRender()}
</div>
<div className="col-md-7">
<div className="card-body">
<h5 className="card-title">{parse(data.title)}</h5>
<div className="card-text">{renderSelf()}</div>
<div className="card-text"><small className="text-muted">By {data.author}</small></div>
<Link to={`/post/${data.id}`}
state={{ from: data}}>
<button className="btn btn-primary">Go to post</button>
</Link>
</div>
</div>
</div>
</div>
)
}
I also stopped using the postId to try and make the API call, and instead only used it to make the dynamic link to the component. After that, I used From's state to grab the permalink(from previous reddit post data) to make the API call to get the comment information.
Here is a snippet of PostDetails.js where I made the changes:
function PostDetails() {
const location = useLocation();
const {from} = location.state;
const comments = useSelector(getSelectedPost);
const dispatch = useDispatch();
useEffect( () => {
dispatch(getPostComments(from.permalink));
}, [dispatch]);
I also found out I was trying to access the data incorrectly in my API call, as the data was structured differently than I originally thought.
Here is the changed thunk in PostSlice.js:
export const getPostComments = createAsyncThunk('posts/getPostComments', async (permalink) => {
const response = await fetch(`http://www.reddit.com${permalink}.json`);
const json = await response.json();
return json[1].data.children.map((comment) => comment.data)
});
There were multiple things wrong with my react app, but hopefully this helps someone else! :)
The second argument to createAsyncThunk is the payloadCreator.
It's called with two arguments, arg which is a single value, and the thunkAPI object.
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
You are passing the thunkAPI argument as the postID value in the GET request.
export const fetchAsyncPostsDetail = createAsyncThunk(
'posts/fetchAsyncPostsDetail',
async (sub, postID) => {
const response = await redditApi.get(`${sub}/comments/${postID}.json`)
return response.data.data.children;
}
);
If you need to pass multiple values to your thunk then pack them into a single object.
export const fetchAsyncPostsDetail = createAsyncThunk(
'posts/fetchAsyncPostsDetail',
async ({ sub, postID }, thunkAPI) => {
const response = await redditApi.get(`${sub}/comments/${postID}.json`)
return response.data.data.children;
}
);
...
useEffect(() => {
dispatch(fetchAsyncPostsDetail({ sub, postID }));
}, [dispatch, postID, sub]);
I am new to Expo and ReactNative. At the moment, I am developing a mobile app that utilizes Firebase Auth and Firestore for authentication and user profile. From googling, I found a solution to use ContextProvider for the Firebase Auth. I tried to adjust the code to obtain the profile at the same time.
This is the code for AuthenticatedUserProvider.js
import React, { useState, createContext } from 'react';
export const AuthenticatedUserContext = createContext({});
export const AuthenticatedUserProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [profile, setProfile] = useState(null);
return (
<AuthenticatedUserContext.Provider value={{ user, setUser, profile, setProfile }}>
{children}
</AuthenticatedUserContext.Provider>
);
};
And I read the profile on the RotNavigator.js below
import React, { useContext, useEffect, useState } from 'react';
import { View, ActivityIndicator } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import * as eva from '#eva-design/eva';
import { ApplicationProvider, IconRegistry } from '#ui-kitten/components';
import { EvaIconsPack } from '#ui-kitten/eva-icons';
import { auth, fs } from '../config/firebase';
import { AuthenticatedUserContext } from './AuthenticatedUserProvider';
import AuthStack from './AuthStack';
import HomeStack from './HomeStack';
import { default as theme } from '../orange-theme.json';
export default function RootNavigator() {
const { user, setUser, profile, setProfile } = useContext(AuthenticatedUserContext);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// onAuthStateChanged returns an unsubscribe
const unsubscribeAuth = auth.onAuthStateChanged(async authenticatedUser => {
try {
if (authenticatedUser) {
await setUser(authenticatedUser)
const response = await fs.collection("members").doc(authenticatedUser.uid).get();
if (response.exists) {
let data = response.data();
// console.log(data);
await setProfile(data);
// console.log(profile);
}
} else {
await setUser(null);
await setProfile(null);
}
setIsLoading(false);
} catch (error) {
console.log(error);
}
});
// unsubscribe auth listener on unmount
return unsubscribeAuth;
}, []);
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size='large' />
</View>
);
}
return (
<>
<IconRegistry icons={EvaIconsPack} />
<ApplicationProvider {...eva} theme={{ ...eva.light, ...theme }}>
<NavigationContainer>
{user ? <HomeStack /> : <AuthStack />}
</NavigationContainer>
</ApplicationProvider >
</>
);
}
The code looks like working except that if I log in, sometimes I saw an error on profile = null and cannot read the profile but then it refreshed by itself and back to run properly.
Can someone suggest the correct way to display the profile from Firestore?
The reason I do it like this is I want an efficient way of displaying the profile since HomeStack has 3 screens that would need to display some parts of the profile on each screen.
Thank you for your help.
I am using react-redux-firebase's fireStoreConnect() middleware with
a screen in my react-native mobile app. At the time of connecting the component to the redux store, I want to specify the firestore sub-collection I connect to, which depends on the user that is navigating the app.
How should I specify the collection in firestoreConnect? The user id is in the redux store.
MWE:
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { compose } from 'redux';
import { connect } from 'react-redux'
import { firestoreConnect } from 'react-redux-firebase';
class PhotosScreen extends Component {
render() {
return (
<View>
<Text> i plan the use this.props.images here </Text>
</View>
);
}
}
const mapStateToProps = (state) => {
// reference the subcollection of the user
const images = state.firestore.data.images;
return {
images: images,
}
}
export default compose(
firestoreConnect([
{
collection: 'users',
doc: "HOW DO I GET THE USERS ID HERE? IT IS IN REDUX STORE",
subcollections: [{ collection: 'images' }]
}
]),
connect(mapStateToProps),
)(PhotosScreen)
Firestore (and all NoSQL databases) follow an alternating "(parent) collection / document / collection / document ..." hierarchical pattern. To synchronize a React component to subcollections and documents below the parent firestore collection, you need to pass the subcollection/subdocument hierarchy information as props to firestoreConnect.
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { compose } from 'redux';
import { connect } from 'react-redux'
import { firestoreConnect } from 'react-redux-firebase';
class PhotosScreen extends Component {
render() {
return (
<View>
<Text> i plan the use this.props.images here </Text>
{images && images.length ? <div> render your images here using this.props.images and images.map </div> : <p>No images</p>}
</View>
);
}
}
const mapStateToProps = (state) => {
return {
images : state.firestore.data.images, // reference the subcollection of the user
userId : state.firestore.auth.uid // assuming the 'doc id' is the same as the user's uid
}
}
export default compose(
firestoreConnect((props) =>
if (!props.userId) return [] // sync only if the userId is available (in this case, if they are authenticated)
return [
{
collection : 'users', // parent collection
doc : props.userId, // sub-document
subcollections : [
{collection : 'images'} // sub-collection
],
storeAs : 'images'
}
]
}),
connect(mapStateToProps),
)(PhotosScreen)
In 1.x
const enhance = compose(
connect(
(state) => ({
someKey: state.someData
})
),
firebaseConnect(
(props, firebaseInstance) => [
{ path: `${props.someKey}/someData` }
]
)
)
In 2.x
firebaseConnect(
(props, store) => [
{ path: `${store.getState().someKey}/someData` }
]
)
Note how the 2nd argument in firebaseConnect changes from firebaseInstance to store from v1 to v2.
This should get you what you need.