I am using "reselect" library in redux to get the data. I wanna get data based on useParams() hook and then to pass it inside mapStatetoProps as ownProps and then to selectionCollection function which is a selector and accepts the value of useParams(). Apparently, it doesn't recognize the useParams() as ownProps and eventually returns undefined. btw, If I pass a string inside selectionCollection('someString), I receive the data but not using useParams() hook. I do receive the useParams value successfully but can't use it inside mapStateToProps which I think is a scope problem. I did define a global variable but didn't work.
import { connect } from 'react-redux';.
import { Outlet, useParams } from 'react-router-dom';
import { selectCollection } from '../../redux/shop/Shop-selectors';
const CollectionPage = ({ collection }) => {
const params = useParams();
const paramsId = params.collectionId;
console.log(collection);
return (
<div className="collection-page">
<p>THis is the collection page for {paramsId}</p>
<Outlet />
</div>
);
};
//-- If I log the 'paramsId', I get undefined since it's outside the function
const mapStateToProps = (state, ownProps) => ({
// here is where I wanna use the paramsId as ownProps
collection: selectCollection(ownProps.paramsId)(state),
});
export default connect(mapStateToProps)(CollectionPage);
I think you can achieve this in different ways but I suggest you two ways to use a params that comes from useParams Hook and mix it with react redux
1- Create another top level component to get the router params in that and pass them to the CollectionPage component, so in the mapStateToProps you can access those params like this:
const TopLevelCollectionPage = () => {
const params = useParams();
<CollectionPage params={params}></CollectionPage>;
};
const CollectionPage = ({ collection }) => {
return (
<div className="collection-page">
<p>THis is the collection page</p>
<Outlet />
</div>
);
};
const mapStateToProps = (state, ownProps) => ({
collection: selectCollection(ownProps.params.collectionId)(state),
});
export default connect(mapStateToProps)(CollectionPage);
2- using react redux hooks to get state inside of your component
const CollectionPage = () => {
const params = useParams();
const collection = useSelector((state) =>
selectCollection(params.collectionId)(state)
);
//or
//const collection = useSelector(selectCollection(params.collectionId));
};
Related
I am trying to pull information from one component's API call to then use that data in another API call in a separate component. However, I am unsure how to export and use the data from the first API call in the second component.
App.js
import './App.css';
import FetchMatch from './fetch-match/fetch.match';
import FetchPlayer from './fetch-player/fetch.player';
function App() {
return (
<div className="App">
<h1>Hello world</h1>
<FetchPlayer></FetchPlayer>
<FetchMatch></FetchMatch>
</div>
);
}
export default App;
fetch.player then makes the first API call to get a users specific ID which will be used in the second API call too fetch that users match history.
fetch.player.js
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const FetchPlayer = () => {
const [playerData, setPlayerData] = useState([]);
const userName = 'users name';
const userTagLine = '1234';
const apiKey = '???';
useEffect( () => {
axios.get(`https://americas.api.riotgames.com/riot/account/v1/accounts/by-riot-id/${userName}/${userTagLine}?api_key=${apiKey}`)
.then(response => {
console.log(response.data)
setPlayerData([response.data])
})
.catch(error => console.log(error))
}, []);
return (
<div>
{playerData.map( data => (
<div>
<p>{data.puuid}</p>
<p>{data.gameName}#{data.tagLine}</p>
</div>
))}
</div>
)
}
export default FetchPlayer;
not much here but just in case...
fetch.match.js
import React, { useState } from 'react';
// Somehow take in the puuid set in the state of fetch.player to make a second API call below
const FetchMatch = () => {
const [matchData, setMatchData] = useState([]);
return (
<div>
// players match list goes here
</div>
)
}
export default FetchMatch;
I am unsure if I should make a separate function instead which would allow me to create consts to handle both API calls in a single file. Or if there is a way to pass the state from fetch.player as a prop to fetch.match from App.js. I have tried to do the former but it either doesn't work or I am messing up the syntax (most likely this)
If you render both component parallelly in a parent component, they are called sibling components.
Data sharing in sibling components can be done by multiple ways (Redux, Context etc) but the easiest and simplest way (the most basic way without 3rd party API) involves the use of parent as a middle component.
First you create the state in the parent component and provide it as props to the child component which need the data from its sibling (in your case is FetchMatch).
import React from 'react';
import './App.css';
import FetchMatch from './fetch-match/fetch.match';
import FetchPlayer from './fetch-player/fetch.player';
function App() {
const [data,setData] = React.useState();
return (
<div className="App">
<h1>Hello world</h1>
<FetchPlayer></FetchPlayer>
<FetchMatch data={data} ></FetchMatch>
</div>
);
}
export default App;
Provide the function to setData as a props to the child component which will fetch the initial API (in your case is FetchPlayer)
<FetchPlayer onPlayerLoad={(data) => setData(data)} />
Then, in that child component when you finish calling the API and get the result, pass that result to the onPlayerLoad function which will call the setData function with the result as parameters. It will lead to state change and re-rendering of the second FetchMatch component feeding the props data with API results.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const FetchPlayer = ({onPlayerLoad}) => {
const [playerData, setPlayerData] = useState([]);
const userName = 'users name';
const userTagLine = '1234';
const apiKey = '???';
useEffect( () => {
axios.get(`https://americas.api.riotgames.com/riot/account/v1/accounts/by-riot-id/${userName}/${userTagLine}?api_key=${apiKey}`)
.then(response => {
console.log(response.data)
setPlayerData([response.data])
onPlayerLoad(response.data)
})
.catch(error => console.log(error))
}, []);
return <></>;
Coming to FetchMatch, you will have the data in its second rendering.
import React, { useState } from 'react';
// Somehow take in the puuid set in the state of fetch.player to make a second API call below
const FetchMatch = ({data}) => {
const [matchData, setMatchData] = useState([]);
//console.log(data);
return (
<div>
// players match list goes here
</div>
)
}
export default FetchMatch;
Now, you can do whatever you want with the shared data in second component which in your case is trigger match API. 🎉
I have implemented a shopping cart using Redux, I have used a dictionary as state object (product id being the key and quantity in cart being the value). Here is how my cart.js looks like:
import React from 'react';
import ReactDOM from 'react-dom';
export const AddItemToCart = (productID) => {
return {
type: 'ADDITEMTOCART',
productID
}
}
export const DeleteItemFromCart = (productID) => {
return {
type: 'DELETEITEMFROMCART',
productID
}
}
export const Counter = (state = {}, action) => {
switch (action.type) {
case 'ADDITEMTOCART':
console.log(action);
return {
...state,
[action.productID]: ( state[action.productID] || 0 ) + 1
}
case 'DELETEITEMFROMCART':
return {
...state,
[action.productID]: ( state[action.productID] || 1 ) - 1
}
}
}
I'm adding an item from App.js like this:
return products.map(products =>
<div key={products.ProductID}>
<h2>{products.ProductName}</h2>
<h2>{products.ProductDescription}</h2>
<h2>{products.ProductQuantity} units available</h2>
<button onClick={() => { store.subscribe(() => console.log(store.getState()));
store.dispatch(AddItemToCart(products.ProductID));}}>Add to cart</button>
</div>
Everything is working just fine but the problem is, I can't render the contents of the cart for user to see. I have tried:
function ShowCartContents() {
var items = Object.keys(store.getState()).map(function(key){
return store.getState()[key];
});
return (
<div>
<h2>{items}</h2>
</div>
);
}
This function throws exception when called:
TypeError: Cannot convert undefined or null to object
Clearly the store itself is not null or undefined, because the change of state is successfully printed to the browser console. So, how would I access all the values in dictionary without keys? And how would I access one specific value by key? Any advise would be highly appreciated. Thanks.
Your Counter reducer has no default case, so your state will be undefined on the first render.
That's the source of your error "TypeError: Cannot convert undefined or null to object".
You need to return the existing state when neither action matches. Every reducer needs a default case because they will be called with actions like the {type: '##redux/INIT'} action which is used to initialize the store.
default:
return state;
You are trying to access the store directly with store.subscribe(), store.getState() and store.dispatch(). This is not the correct way to interact with a Redux store in React. You should use the react-redux package.
You want to wrap your entire app in a Provider component that provides the store instance. Something like this:
import { render } from "react-dom";
import { Provider } from "react-redux";
import App from "./components/App";
import store from "./store";
const rootElement = document.getElementById("root");
render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
In your components, use the hook useSelector to select values from the state and useDispatch to access the dispatch function. (You can also use the connect higher-order component, but the hooks are preferred).
I'm not sure if this reducer is your entire state or if you are using combineReducers and have multiple reducers like cart, products, etc. This selector is assuming that it's the entire state.
function ShowCartContents() {
const productIds = useSelector(state => Object.keys(state))
return (
<div>
<h2>Ids In Cart: {productIds.join(", ")}</h2>
</div>
);
}
function ProductsList({ products }) {
const dispatch = useDispatch();
return (
<div>
{products.map((product) => (
<div key={product.ProductID}>
<h2>{product.ProductName}</h2>
<h2>{product.ProductDescription}</h2>
<h2>{product.ProductQuantity} units available</h2>
<button onClick={() => dispatch(AddItemToCart(product.ProductID))}>
Add to cart
</button>
</div>
))}
</div>
);
}
I have this class component that returns state populated perfectly using Redux store:
class TopRatedComponent extends Component {
componentDidMount() {
this.props.fetchTopRatedMovies();
}
render() {
const IMG_API_ROOT_LINK = 'https://image.tmdb.org/t/p/w500';
const { topRatedMovies, loading, error } = this.props;
return (
<div>
{loading && <div>LOADING...</div>}
{error && <div>{error}</div>}
<Container className="p-4" onScroll={this.onScroll}>
<div className="text-center">
{
topRatedMovies.results && topRatedMovies.results.map(topRated => (
<p>{topRated.title}</p>
))
}
</div>
</Container>
</div>
);
}
}
const mapStateToProps = state => {
const { topRatedMovies, loading, error } = state.topRatedMovies;
return {
topRatedMovies,
loading,
error
};
};
export default connect(
mapStateToProps,
{
fetchTopRatedMovies
}
)(TopRatedComponent);
However, when I switch the above class component into a functional component below so I can use ReactJS hooks with my code, but the state is always empty.
const TopRatedComponent = () => {
const [count, setCount] = useState(0);
const [topRated, settopRated] = useState([]);
useEffect(() => {
settopRated(this.props.fetchTopRatedMovies)
}, [this.props.fetchTopRatedMovies])
const allState = useSelector((state) => state)
console.log('CHOF: ' + JSON.stringify(allState));
return (
<div>
WOOOOOOOW....
</div>
)
};
You haven't correctly transformed your class-based component into equivalent functional component.
Following are the problems in your functional component:
In class component, you receive the fetchTopRatedMovies action creator as a prop and you dispatch it from the componentDidMount lifecycle method. In functional component, you are not dispatching it.
To dispatch the action in functional components, use useDispatch() hook and use useEffect hook to dispatch this action after component has mounted.
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
const TopRatedComponent = () => {
...
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchTopRatedMovies());
}, []);
...
};
In class components, you access props object using this but in functional components, props object is passed as an argument. So, you can directly access it using the parameter name you use for the props object
const TopRatedComponent = (props) => {
console.log(props);
// code
};
Data that your component receives as props from the redux store using mapStateToProps function and connect higher order component, can be accessed using useSelector hook in functional components.
import { useSelector } from "react-redux";
const TopRatedComponent = () => {
const {
topRatedMovies,
loading,
error
} = useSelector(state => state.topRatedMovies);
// code
};
Note: You can also use connect higher-order component and mapStateToProps to connect your functional component with the redux store.
For details of how to use hooks with react-redux, see: react-redux - Hooks
this doesn't work the same way in a functional component. You need to pull props from your function arguments instead.
const TopRatedComponent = (props) => {
const [count, setCount] = useState(0);
const [topRated, settopRated] = useState([]);
useEffect(() => {
settopRated(props.fetchTopRatedMovies)
}, [props.fetchTopRatedMovies])
const allState = useSelector((state) => state)
console.log('CHOF: ' + JSON.stringify(allState));
return (
<div>
WOOOOOOOW....
</div>
)
};
Arrow functions don't have their own context (this variable). I assume that fetchTopRatedMovies is an action (thunk) that fetches data from an API and set it to a global state. In this case, you need to also get that data using Redux hooks (if the version you are using supports it).
const TopRatedComponent = ({ fetchTopRatedMovies }) => {
const [count, setCount] = useState(0);
// this is written according to your `mapStateToProps` function.
const { topRatedMovies, loading, error } = useSelector(({topRatedMovies}) => topRatedMovies);
useEffect(() => {
fetchTopRatedMovies();
}, [fetchTopRatedMovies])
if (loading) return 'LOADING...';
if (error) return <div>{error.message}</div>
return (
<div>
etc...
</div>
)
};
I have an object "user" in reduxReducer and i want to use it in a functional component.
I use this object by using mapStateToProps as following:
const mapStateToProps = (state) => ({
login: state.login,
});
Follwing is the component in which i wanted to use this object.
const ProfilesAdministration = (props, { login: { user }}) => {
// Code Here
}
const mapStateToProps = (state) => ({
login: state.login,
});
export default connect(mapStateToProps , null)( ProfilesAdministration );
Please Help.. Thanks
You should access it from props. Redux connect argument mapStateToProps will add the result from your selector to the components props:
const ProfilesAdministration = (props) => {
// or const user = props.login.user
const { user } = props.login
// Code Here
}
Connect makes your component like having additional props from the redux store.
I'm using the context API and I have this in my context file:
useEffect(() => {
async function getSingleCountryData() {
const result = await axios(
`https://restcountries.eu/rest/v2/alpha/${props.match.alpha3Code.toLowerCase()}`
);
console.log(result)
setCountry(result.data);
}
if (props.match) getSingleCountryData();
}, [props.match]);
In the component I'm using, it doesn't work because it doesn't know what the props.match.alpha3Code is. How can I can pass the value? The alpha3Code is coming from the URL: localhost:3000/country/asa where asa is the alpha3Code, how can I get this value?
Basically, what I'm trying to do is. I have a list of countries I listed out on the home page. Now I'm trying to get more information about a single country. The route is /country/:alpha3Code where alpha3Code is gotten from the API.
FWIW, here is my full context file:
import React, { useState, createContext, useEffect } from 'react';
import axios from 'axios';
export const CountryContext = createContext();
export default function CountryContextProvider(props) {
const [countries, setCountries] = useState([]);
const [country, setCountry] = useState([]);
useEffect(() => {
const getCountryData = async () => {
const result = await axios.get(
'https://cors-anywhere.herokuapp.com/https://restcountries.eu/rest/v2/all'
);
setCountries(result.data);
};
getCountryData();
}, []);
useEffect(() => {
async function getSingleCountryData() {
const result = await axios(
`https://restcountries.eu/rest/v2/alpha/${props.match.alpha3Code.toLowerCase()}`
);
console.log(result)
setCountry(result.data);
}
if (props.match) getSingleCountryData();
}, [props.match]);
return (
<CountryContext.Provider value={{ countries, country }}>
{props.children}
</CountryContext.Provider>
);
}
In the component I'm using the country, I have:
const { country } = useContext(CountryContext);
I know I can do this from the component itself, but I'm learning how to use the context API, so I'm handling all API calls in my context.
The API I'm making use of is here
Codesandbox Link
Project Github link
You can update the context from a component using it by passing down a setter function which updates the context state.
export default function CountryContextProvider({ children }) {
const [countries, setCountries] = useState([]);
const [country, setCountry] = useState([]);
const [path, setPath] = useState('');
useEffect(() => {
async function getSingleCountryData() {
const result = await axios(`your/request/for/${path}`);
setCountry(result.data);
}
if(path) getSingleCountryData();
}, [path]);
return (
<CountryContext.Provider value={{ countries, country, setPath }}>
{children}
</CountryContext.Provider>
);
}
Now use setPath to update the request endpoint with the route match once this component is mounted.
const Details = ({ match }) => {
const {
params: { alpha3Code }
} = match;
const { country, setPath } = useContext(CountryContext);
useEffect(() => {
setPath(alpha3Code);
}, [alpha3Code]);
return (
<main>Some JSX here</main>
);
};
export default withRouter(Details);
Linked is a working codesandbox implementation
In the component I'm using, it doesn't work because it doesn't know
what the props.match.alpha3Code is. How can I can pass the value? The
alpha3Code is coming from the URL: localhost:3000/country/asa where
asa is the alpha3Code, how can I get this value?
I guess the root of your problem is this one. You have no idea which the aplha3Code parameter comes from. I have dived into your GitHub repo to make it clearer.
First, match is one of react-router provided terms. When you use something like props.match, props.history, props.location, you must have your component wrapped by the withRouter, which is a Higher Order Component provided by react-router. Check it out at withRouter. For example, below is the withRouter usage which is provided by react-router:
// A simple component that shows the pathname of the current location
class ShowTheLocation extends React.Component {
render() {
const { match, location, history } = this.props;
return <div>You are now at {location.pathname}</div>;
}
}
const ShowTheLocationWithRouter = withRouter(ShowTheLocation);
ShowTheLocation is wrapped by the withRouter HOC, which will pass all the route props (match, history, location...) to ShowTheLocation through props. Then inside ShowTheLocation, you are able to use something like props.match. Clear enough?
So back to your problem! You have not wrapped any components by withRouter yet, have you? Stick to it and have some fun! You will figure it out soon!
Also, please be aware that you must place your component under the BrowserRouter to be able to use the react-router things
If you want to go with Hooks, please take a look at this super useful one:
https://usehooks.com/useRouter/
It wraps all the useParams, useLocation, useHistory, and use useRouteMatch hooks up into a single useRouter that exposes just the data and methods we need. Then, for example, inside your component, do it like this:
import { useRouter } from "./myCustomHooks";
const ShowMeTheCode = () => {
const router = useRouter();
return <div>This is my alpha3Code: {router.math.params.alpha3Code}</div>;
}
Update 1 from Peoray's reply:
This is where the problem occurs:
https://github.com/peoray/where-in-the-world/blob/cb09871fefb2f58f5cf0a4f1db3db2cc5227dfbe/src/pages/Details.js#L6
You should avoid calling useContext() straightly like that. Have a look at my example below:
// CountryContext.js
import { useContext, createContext } from "react";
const CountryContext = createContext();
export const useCountryContext = () => useContext(CountryContext);
Instead, you should wrap it by a custom hook like useCountryContext above. And then, inside your Details component, import it and do like:
import React, from 'react';
import { useCountryContext } from '../contexts/CountryContext';
const Details = (props) => {
const { country } = useCountryContext();
...
}
Update 2 from Peoray's reply:
Although I have stated it in advance for you, I just feel like you did not make enough effort to go through what I said.
Also, please be aware that you must place your component under the
BrowserRouter to be able to use the react-router things
In your codesandbox, it shows the Cannot read property 'match' of undefined error. Okay, as I said above, you have not moved the ContextCountryProvider to under the BrowserRouter to get the useRouter work.
I have fixed it for you, and the screen popped out, please check it at updated codesanbox here. You will get what you need at App.js file.
Although it still throws some Axios bugs there, I think my job is done. The rest is up to you.
You might use useParams hook to get everything you need inside your context provider. Docs
Something like this:
import useParams in file where your Provider component is
in your CountryContextProvider add this at the top of the component:
const { alpha3Code } = useParams();
update useEffect which needs props.match
useEffect(() => {
async function getSingleCountryData() {
const result = await axios(
`https://restcountries.eu/rest/v2/alpha/${alpha3Code.toLowerCase()}`
);
console.log(result)
setCountry(result.data);
}
if (alpha3Code) getSingleCountryData(); // or if you need `match` - do not destructure useParams()
}, [alpha3Code]);