Getting undefined props in functional react components - javascript

How to pass the {requests} prop to the RequestRow component after executing the setRequests? My understanding is that the requests get initialized as undefined in the beginning and before being set with the asynchronously called object, it gets passed to the RequestRow component as undefined, and the error occurs.
import React, { useState, useEffect } from 'react';
import 'semantic-ui-css/semantic.min.css';
import Layout from '../../../components/Layout';
import { Button } from 'semantic-ui-react';
import { Link } from '../../../routes';
import Campaign from '../../../blockchain/campaign';
import { Table } from 'semantic-ui-react';
import RequestRow from '../../../components/RequestRow';
const RequestsIndex = ({ address }) => {
const { Header, Row, HeaderCell, Body } = Table;
const campaign = Campaign(address);
const [requestCount, setRequestCount] = useState();
const [requests, setRequests] = useState([]);
const getRequests = async () => {
const count = await campaign.methods.getRequestsCount().call();
setRequestCount(count);
};
let r;
const req = async () => {
r = await Promise.all(
Array(parseInt(requestCount))
.fill()
.map((_element, index) => {
return campaign.methods.requests(index).call();
})
);
setRequests(r);
};
useEffect(() => {
getRequests();
if (requestCount) {
req();
}
}, [requestCount]);
return (
<Layout>
<h3>Requests List.</h3>
<Link route={`/campaigns/${address}/requests/new`}>
<a>
<Button primary>Add Request</Button>
</a>
</Link>
<Table>
<Header>
<Row>
<HeaderCell>ID</HeaderCell>
<HeaderCell>Description</HeaderCell>
<HeaderCell>Amount</HeaderCell>
<HeaderCell>Recipient</HeaderCell>
<HeaderCell>Approval Count</HeaderCell>
<HeaderCell>Approve</HeaderCell>
<HeaderCell>Finalize</HeaderCell>
</Row>
</Header>
<Body>
<Row>
<RequestRow requests={requests}></RequestRow>
</Row>
</Body>
</Table>
</Layout>
);
};
export async function getServerSideProps(context) {
const address = context.query.address;
return {
props: { address },
};
}
export default RequestsIndex;
The RequestRow component is shown below. It takes in the {requests} props, which unfortunately is undefined.
const RequestRow = ({ requests }) => {
return requests.map((request, index) => {
return (
<>
<div>Request!!!</div>
</>
);
});
};
export default RequestRow;
The snapshot of the error is shown below:

I think React is trying to render your component before your promises resolve. If that's the case, all you need to do is set a default value (an empty array in your case) for your requests.
const [requests, setRequests] = useState([]);
May the force be with you.

Related

How can I send the state (useState) of one file component to another file's component?

REACT.js:
Let say I have a home page with a search bar, and the search bar is a separate component file i'm calling.
The search bar file contains the useState, set to whatever the user selects. How do I pull that state from the search bar and give it to the original home page that
SearchBar is called in?
The SearchBar Code might look something like this..
import React, { useEffect, useState } from 'react'
import {DropdownButton, Dropdown} from 'react-bootstrap';
import axios from 'axios';
const StateSearch = () =>{
const [states, setStates] = useState([])
const [ stateChoice, setStateChoice] = useState("")
useEffect (()=>{
getStates();
},[])
const getStates = async () => {
let response = await axios.get('/states')
setStates(response.data)
}
const populateDropdown = () => {
return states.map((s)=>{
return (
<Dropdown.Item as="button" value={s.name}>{s.name}</Dropdown.Item>
)
})
}
const handleSubmit = (value) => {
setStateChoice(value);
}
return (
<div>
<DropdownButton
onClick={(e) => handleSubmit(e.target.value)}
id="state-dropdown-menu"
title="States"
>
{populateDropdown()}
</DropdownButton>
</div>
)
}
export default StateSearch;
and the home page looks like this
import React, { useContext, useState } from 'react'
import RenderJson from '../components/RenderJson';
import StateSearch from '../components/StateSearch';
import { AuthContext } from '../providers/AuthProvider';
const Home = () => {
const [stateChoice, setStateChoice] = useState('')
const auth = useContext(AuthContext)
console.log(stateChoice)
return(
<div>
<h1>Welcome!</h1>
<h2> Hey there! Glad to see you. Please login to save a route to your prefered locations, or use the finder below to search for your State</h2>
<StateSearch stateChoice={stateChoice} />
</div>
)
};
export default Home;
As you can see, these are two separate files, how do i send the selection the user makes on the search bar as props to the original home page? (or send the state, either one)
You just need to pass one callback into your child.
Homepage
<StateSearch stateChoice={stateChoice} sendSearchResult={value => {
// Your Selected value
}} />
Search bar
const StateSearch = ({ sendSearchResult }) => {
..... // Remaining Code
const handleSubmit = (value) => {
setStateChoice(value);
sendSearchResult(value);
}
You can lift the state up with function you pass via props.
const Home = () => {
const getChoice = (choice) => {
console.log(choice);
}
return <StateSearch stateChoice={stateChoice} giveChoice={getChoice} />
}
const StateSearch = (props) => {
const handleSubmit = (value) => {
props.giveChoice(value);
}
// Remaining code ...
}
Actually there is no need to have stateChoice state in StateSearch component if you are just sending the value up.
Hello and welcome to StackOverflow. I'd recommend using the below structure for an autocomplete search bar. There should be a stateless autocomplete UI component. It should be wrapped into a container that handles the search logic. And finally, pass the value to its parent when the user selects one.
// import { useState, useEffect } from 'react' --> with babel import
const { useState, useEffect } = React // --> with inline script tag
// Autocomplete.jsx
const Autocomplete = ({ onSearch, searchValue, onSelect, suggestionList }) => {
return (
<div>
<input
placeholder="Search!"
value={searchValue}
onChange={({target: { value }}) => onSearch(value)}
/>
<select
value="DEFAULT"
disabled={!suggestionList.length}
onChange={({target: {value}}) => onSelect(value)}
>
<option value="DEFAULT" disabled>Select!</option>
{suggestionList.map(({ id, value }) => (
<option key={id} value={value}>{value}</option>
))}
</select>
</div>
)
}
// SearchBarContainer.jsx
const SearchBarContainer = ({ onSelect }) => {
const [searchValue, setSearchValue] = useState('')
const [suggestionList, setSuggestionList] = useState([])
useEffect(() => {
if (searchValue) {
// some async logic that fetches suggestions based on the search value
setSuggestionList([
{ id: 1, value: `${searchValue} foo` },
{ id: 2, value: `${searchValue} bar` },
])
}
}, [searchValue, setSuggestionList])
return (
<Autocomplete
onSearch={setSearchValue}
searchValue={searchValue}
onSelect={onSelect}
suggestionList={suggestionList}
/>
)
}
// Home.jsx
const Home = ({ children }) => {
const [result, setResult] = useState('')
return (
<div>
<SearchBarContainer onSelect={setResult} />
result: {result}
</div>
)
}
ReactDOM.render(<Home />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Just pass a setState to component
parent component:
const [state, setState] = useState({
selectedItem: ''
})
<StateSearch state={state} setState={setState} />
change parent state from child component:
const StateSearch = ({ state, setState }) => {
const handleStateChange = (args) => setState({…state, selectedItem:args})
return (...
<button onClick={() => handleStateChange("myItem")}/>
...)
}

Problem when maping array - React with Redux Toolkit

I have the following problem: I use a fatch API to get all the products I have registered in my database (mongodb), then I store the result in a slice called products-slice which has an array as its initial state empty. Until then everything is in order. As I need information the time the homepage is loaded, I use the useEffect hook to fetch the products I have registered. Then I pass this array as props to a component, and make a map. The problem is that when the component loads, the information is not local.
código do backend
module.exports.fetchProduct = async (req, res) => {
try {
const products = await Product.find({});
if (products) {
res.json(products);
}
} catch (error) {
console.log(error);
}
};
productsActions.js
export const fetchProducts = () => {
return async (dispatch) => {
try {
const response = await fetch("http://localhost:xxxx/xxxxxx");
const data = await response.json();
let loadedProducts = [];
for (const key in data) {
loadedProducts.push({
id: data[key]._id,
productName: data[key].productName,
price: data[key].price,
imageUrl: data[key].imageUrl,
});
}
dispatch(setProducts(loadedProducts));
} catch (error) {
console.log(error);
}
};
};
home.jsx
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Container } from "react-bootstrap";
import {fetchProducts} from '../../store/actions/productsActions';
import Hero from "../hero/Hero";
import Footer from "../footer/Footer";
import DisplayProductsList from "../displayProduct/DisplayProductsList";
export default function Home() {
const productsInfo = useSelector((state) => state.products.products);
console.log(productsInfo);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProducts());
}, [dispatch]);
return (
<>
<Hero />
<DisplayProductsList products={productsInfo} />
<Container fluid>
<Footer></Footer>
</Container>
</>
);
}
product-slice.js
const initialState = {
products: [],
};
const productSlice = createSlice({
name: "product",
initialState,
reducers: {
setProducts(state, action) {
state.products.push(action.payload);
},
},
});
export const { setProducts } = productSlice.actions;
export default productSlice.reducer;
component where I'm mapping
export default function DisplayProductsList(props) {
console.log(props);
return (
props.products.map((product) => (
<DisplayProducts
key={product.id}
imageUrl={product.imageUrl}
name={product.productName}
price={product.price}
/>
))
);
}
console.log output in the above component
enter image description here

How to pass data from child to parent and render content based on selected value in dropdown?

I am learning React as I am fetching data from Pokéapi to make a list component, card component, detail component and filter component. I am trying to make a filter so you can filter by pokémon type. Only the cards that also contain that type string should then render (Not there yet). So I am not sure if a) I should make a different call from API inside PokemonList depending on selected value or b) if I should compare the values and just change how the PokemonCard element is rendered inside PokemonList.js depending on the comparison. I managed to pass data from filter to the list component. I have then been trying to pass the type data from PokemonCard.js to the list component so that I can compare these two values but I find it hard to use callbacks to pass the type data from the card component, since I dont pass it through an event or something like that.
Which method should I use here to simplify the filtering? Make different API call or render PokemonCard element conditionally?
Is it a good idea to compare filter option to pokemon card's type in PokemonList.js? Then how can I pass that data from the card component since I don't pass it through click event?
Thankful for any ideas! I paste the code from list component that contains the cards, card component and filter component.
PokemonList component:
import { useState } from 'react';
import useSWR from 'swr';
import PokemonCard from './PokemonCard';
import PokemonFilter from './PokemonFilter';
import './PokemonList.css';
const PokemonList = () => {
const [index, setIndex] = useState(0);
const [type, setType] = useState('');
function selectedType(type) { // value from filter dropdown
setType(type)
console.log("handled")
console.log(type)
}
const url = `https://pokeapi.co/api/v2/pokemon?limit=9&offset=${index}`;
const fetcher = (...args) => fetch(...args).then((res) => res.json())
const { data: result, error } = useSWR(url, fetcher);
if (error) return <div>failed to load</div>
if (!result) return <div>loading...</div>
result.results.sort((a, b) => a.name < b.name ? -1 : 1);
return (
<section>
<PokemonFilter onSelectedType={selectedType} selectedPokemonType={type} />
<div className="pokemon-list">
<div className="pokemons">
{result.results.map((pokemon) => (
<PokemonCard key={pokemon.name} pokemon={pokemon} /> // callback needed??
))}
</div>
<div className="pagination">
<button
onClick={() => setIndex(index - 9)}
disabled={result.previous === null}
>
Previous
</button>
<button
onClick={() => setIndex(index + 9)}
disabled={result.next === null}
>
Next
</button>
</div>
</div>
</section>
)
}
export default PokemonList;
PokemonCard component:
import { Link } from "react-router-dom";
import useSWR from 'swr';
import './PokemonCard.css';
const PokemonCard = ({ pokemon }) => {
const { name } = pokemon;
const url = `https://pokeapi.co/api/v2/pokemon/${name}`;
const { data, error } = useSWR(url);
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
const { types, abilities } = data;
// types[0].type.name <---- value I want to pass to PokemonList.js
return (
<div className='pokemon-card'>
<div className='pokemon-card__content'>
<img
className='pokemon-card__image'
src={data.sprites.front_default}
alt={name}
/>
<div className='pokemon-card__info'>
<p className='pokemon-card__name'>Name: {name}</p>
<p className='pokemon-card__abilities'>Abilities: {abilities[0].ability.name}</p>
<p className='pokemon-card__categories'>Category: {types[0].type.name}</p>
</div>
</div>
<Link className='pokemon-card__link' to={{
pathname: `/${name}`,
state: data
}}>
View Details
</Link>
</div>
)
}
export default PokemonCard;
PokemonFilter component:
import './PokemonFilter.css';
import useSWR from 'swr';
const PokemonFilter = ({onSelectedType, selectedPokemonType}) => {
const url = `https://pokeapi.co/api/v2/type/`;
const fetcher = (...args) => fetch(...args).then((res) => res.json())
const { data: result, error } = useSWR(url, fetcher);
if (error) return <div>failed to load</div>
if (!result) return <div>loading...</div>
function filteredTypeHandler(e) {
console.log(e.target.value);
onSelectedType(e.target.value);
}
console.log(selectedPokemonType)
return(
<div className="pokemon-types__sidebar">
<h2>Filter Pokémon by type</h2>
<select
name="pokemon-type"
className="pokemon-types__filter"
onChange={filteredTypeHandler}
>
<option value="All">Filter By Type</option>
{result.results.map((type) => {
return (
<option key={type.name} value={type.name}> {type.name}</option>
)
})}
</select>
</div>
)
}
export default PokemonFilter;
Here is an example to improve, modify, ... I didn't test, it's just a visual example.
I don't know about useSWR sorry, I use axios in my example...
If you want to centralize all your API requests, you can create a useApi hook, on the internet you will find tutorials.
PokemonList.js
import React, { useState, useEffect } from 'react';
import axios from 'axios'; // or swr
import PokemonFilter from './PokemonFilter';
import PokemonCard from './PokemonCard';
export default function PokemonList() {
const [data, setData] = useState([]);
const [filter, setFilter] = useState('');
// Executed every first render
useEffect(() => {
getData();
}, []);
// Executed only when filter changes
useEffect(() => {
getDataByTypes(filter);
}, [filter]);
// Get data
const getData = async () => {
const uri = 'https://xxx';
try {
const response = await axios.get(uri);
setData(response.data...);
} catch (error) {
console.log(error);
}
};
// Get data by types
const getDataByTypes = async (filter) => {
const uri = `https://xxx/type/${filter}...`;
if (filter) {
try {
const response = await axios.get(uri);
setData(response.data...);
} catch (error) {
console.log(error);
}
}
};
return (
<div className="main">
<PokemonFilter filter={filter} setFilter={setFilter} />
<div className="container">
<div className="cards-container">
{data.map((d) => (
<PokemonCard key={d.name} data={d} />
))}
</div>
</div>
</div>
);
}
PokemonCard.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
export default function PokemonCard({ data }) {
const [pokemons, setPokemons] = useState();
useEffect(() => {
getPokemons(data);
}, [data]);
// Get Pokemons
const getPokemons = async (data) => {
const uri = `https://xxx/pokemon/${data.name}/`;
try {
const response = await axios.get(uri);
setPokemons(response.data...);
} catch (error) {
console.log(error);
}
};
return (
<div>
{pokemons && (
<div className="card">
<img src={pokemons.sprites.front_default} alt={pokemons.name} />
<p>{pokemons.name}</p>
<p>{pokemons.abilities[0].ability.name}</p>
<p>{pokemons.types[0].type.name}</p>
</div>
)}
</div>
);
}
PokemonFilter.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
export default function PokemonFilter({ filter, setFilter }) {
const [types, setTypes] = useState([]);
useEffect(() => {
getType();
}, []);
// Get Type
const getType = async () => {
const uri = 'https://xxx/type/';
try {
const response = await axios.get(uri);
setTypes(response.data.results....);
} catch (error) {
console.log(error);
}
};
const handleFilter = (e) => {
setFilter(e.target.value);
};
return (
<select onChange={handleFilter} value={filter}>
<option>Filter by type</option>
{types.map((type) => {
return (
<option key={type.name} value={type.name}>
{type.name}
</option>
);
})}
</select>
);
}

Display the data from 'this.state.data'?

Goal:
*Get the data of of variable Cars to the 'this.state.data' when you have retrieved the data from API.
*Display data from 'this.state.data' and not using the variable Cars.
Problem:
I do not know how to do it and is is it possible to do it when you have applied refactoring SOLID?
Info:
I'm newbie in React JS.
Stackblitz:
https://stackblitz.com/edit/react-v39jre?
App.js
import React from 'react';
import './style.css';
import CarsList from './components/CarsList';
import React, { Component } from 'react';
class App extends Component {
constructor() {
super();
this.state = {
name: 'React',
data: null
};
}
render() {
return (
<div className="App">
<CarsList />
</div>
);
}
}
export default App;
CarsList.jsx
import React, { useState, useEffect } from 'react';
this.state = {
name: 'React',
data: null
};
const CarsList = () => {
const [cars, setCars] = useState([]);
useEffect(() => {
const fetchCars = async () => {
const response = await fetch(
'https://jsonplaceholder.typicode.com/users'
);
setCars(await response.json());
};
fetchCars();
}, []);
return (
<div>
{cars.map((car, index) => (
<li key={index}>
[{++index}]{car.id} - {car.name}$
</li>
))}
</div>
);
};
export default CarsList;
After getting the response in the child component you should do a callback function which can be passed as prop from parent to child. Using the function you can pass the data from child to parent and update the parent state.
App.js
import { useState } from "react";
import CarsList from "./CarsList";
import "./styles.css";
export default function App() {
const [state, setState] = useState([]);
const handleUpdateParentState = (data) => {
setState(data);
};
console.log("state in parent", state);
return (
<div>
<CarsList updateParentState={handleUpdateParentState} />
</div>
);
}
CarsList.js
import React, { useState, useEffect } from "react";
const CarsList = (props) => {
const [cars, setCars] = useState([]);
useEffect(() => {
const fetchCars = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
const data = await response.json();
setCars(data);
props?.updateParentState(data);
} catch (error) {
console.log(error);
}
};
fetchCars();
}, []);
return (
<ul>
{cars?.map((car, index) => (
<li key={index}>
[{++index}]{car.id} - {car.name}$
</li>
))}
</ul>
);
};
export default CarsList;
Codesandbox
Data can be shared using props but from parent component to child component only. We cannot pass child component state to parent component through props.
Though we can create a function at parent level and pass it to child component as props so we can execute there.
In your case, you have to create a function in App component and pass it on carList component as props. In carList component you do not have to create the cars state. After fetching the cars from API just call the function you passed from App component
App.js
import React from 'react';
import './style.css';
import CarsList from './components/CarsList';
import React, { Component } from 'react';
class App extends Component {
constructor() {
super();
this.state = {
name: 'React',
data: null
};
}
function setCarList(cars) {
this.setState({
date: cars
});
}
render() {
return (
<div className="App">
<CarsList setCars={setCarList}/>
</div>
);
}
}
export default App;
CarList.js
import React, {useEffect } from 'react';
this.state = {
name: 'React',
data: null
};
const CarsList = (props) => {
useEffect(() => {
const fetchCars = async () => {
const response = await fetch(
'https://jsonplaceholder.typicode.com/users'
);
this.props.setCars(await response.json());
};
fetchCars();
}, []);
return (
<div>
{cars.map((car, index) => (
<li key={index}>
[{++index}]{car.id} - {car.name}$
</li>
))}
</div>
);
};
export default CarsList;
It doesn't make much sense for each CarList component to load data if you're going to have loads of them and they're going to share information with each other. You should load all your data in your App component using an array of API fetch calls and then use Promise.all to extract and parse the data, and then add it to the state. That state can be then shared with all your Carlist components.
Here's a React component:
const {Component} = React;
const json = '["BMW", "Clio", "Merc", "Fiat"]';
// Simulates an API call
function mockFetch() {
return new Promise((res, rej) => {
setTimeout(() => res(json), 1000);
});
}
class App extends Component {
constructor() {
super();
this.state = { cars: [] };
}
componentDidMount() {
// Have an array fetches (you would supply each one a
// different API endpoint in your code)
const arr = [mockFetch(), mockFetch(), mockFetch()];
// Grab the json, `map` over it and parse it
Promise.all(arr).then(data => {
const cars = data.map(arr => JSON.parse(arr));
// Then set the new state
this.setState(prev => ({ ...prev, cars }));
});
}
// You can now send the data to your small functional
// carlist components
render() {
const { cars } = this.state;
if (!cars.length) return <div />;
return (
<div>
<Carlist cars={cars[0]} />
<Carlist cars={cars[1]} />
<Carlist cars={cars[2]} />
</div>
)
}
};
function Carlist({ cars }) {
return (
<ul>{cars.map(car => <div>{car}</div>)}</ul>
);
}
// Render it
ReactDOM.render(
<App />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
And here's equivalent written as a functional component with hooks:
const {useState, useEffect} = React;
const json = '["BMW", "Clio", "Merc", "Fiat"]';
function mockFetch() {
return new Promise((res, rej) => {
setTimeout(() => res(json), 1000);
});
}
function App() {
const [cars, setCars] = useState([]);
// This works in the same way as the previous example
// except we're not setting `this.state` we're setting the
// state called `cars` that we set up with `useState`.
useEffect(() => {
function getData() {
const arr = [mockFetch(), mockFetch(), mockFetch()];
Promise.all(arr).then(data => {
const cars = data.map(arr => JSON.parse(arr));
setCars(cars);
});
}
getData();
}, []);
if (!cars.length) return <div />;
return (
<div>
<Carlist cars={cars[0]} />
<Carlist cars={cars[1]} />
<Carlist cars={cars[2]} />
</div>
);
};
function Carlist({ cars }) {
return (
<ul>{cars.map(car => <div>{car}</div>)}</ul>
);
}
// Render it
ReactDOM.render(
<App />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Cannot read property 'map' of undefined - React Custom Hooks / useContext one works / one doesn't

I am currently following a tutorial on youtube building a whatsapp clone with react and socket.io from webDevSimplified: https://www.youtube.com/watch?v=tBr-PybP_9c
and here the main repo : 'https://github.com/WebDevSimplified/Whatsapp-Clone/tree/master/client
I got stuck halfway through as for some reason my custom useConversation hook returns undefined while my useContacts hook works without any problems.
Here the setup:
App.js :
import Dashboard from "./Dashboard";
import { ContactsProvider } from "../contexts/ContactsProvider";
import { ConversationsProvider } from "../contexts/ConversationsProvider";
import Test from "../components/test";///test
console.log(Test)//test purpose
function App() {
const [id, setId] = useLocalStorage("id");
const dashboard = (
<ContactsProvider>
<ConversationsProvider id={id}>
<Dashboard id={id} />
</ConversationsProvider>
</ContactsProvider>
);
return id ? dashboard : <Login onIdSubmit={setId} />;
}
export default App;
ContactsProvider.js
import React, { useContext } from "react";
import useLocalStorage from "../hooks/useLocalStorage";
const ContactsContext = React.createContext();
export function useContacts() {
return useContext(ContactsContext);
}
export function ContactsProvider({ children }) {
const [contacts, setContacts] = useLocalStorage("contacts", []);//initalValue an empty array
function createContact(id, name) {
setContacts((prevContacts) => {
return [...prevContacts, { id, name }];
});
}
return (
<ContactsContext.Provider value={{ contacts, createContact }}>
{children}
</ContactsContext.Provider>
);
}
Contacts.js - here my useContacts works
import React from "react";
import { ListGroup } from "react-bootstrap";
import { useContacts } from "../contexts/ContactsProvider";
export default function Contacts() {
const { contacts } = useContacts();
console.log(`contacts: ${contacts}`); //returns object, object as expected
return (
<ListGroup variant="flush">
{contacts.map((contact) => (//visibly working in UI when commenting out the ListGroup in conversations.js
<ListGroup.Item key={contact.id}>{contact.name}</ListGroup.Item>
))}
</ListGroup>
);
}
Here the problematic part:
ConversationsProvider.js
import React, { useContext } from "react";
import useLocalStorage from "../hooks/useLocalStorage";
import { useContacts } from "./ContactsProvider";
const ConversationsContext = React.createContext();
export function useConversations() {
return useContext(ConversationsContext);
}
export function ConversationsProvider({ children }) {
const [conversations, setConversations] = useLocalStorage(
"conversations", []);//as well empty array
const { contacts } = useContacts();
function createConversation(recipients) {
setConversations((prevConversations) => {
return [...prevConversations, { recipients, messages: [] }];
});
}
const formattedConversations = conversations.map((conversation) => {
const recipients = conversation.recipients.map((recipient) => {
const contact = contacts.find((contact) => {
return contact.id === recipient;
});
const name = (contact && contact.name) || recipient;
return { id: recipient, name };
});
return { ...conversation, recipients };
});
const value = {
conversations: formattedConversations,
createConversation,
};
return (
<ConversationsContext.Provider value={{ value }}>
{children}
</ConversationsContext.Provider>
);
}
and the component that causes the error:
Conversations.js:
import React from "react";
import { ListGroup } from "react-bootstrap";
import { useConversations } from "../contexts/ConversationsProvider";
export default function Conversations() {
const { conversations } = useConversations();
console.log( `conversations: ${conversations}`)//returns undefined
return (
<ListGroup variant="flush">
{conversations.map((conversation, index) => (//can't map because conversations is undefined
<ListGroup.Item key={index}>
{conversation.recipients
.map(r => r.name)
.join(", ")}
</ListGroup.Item>
))}
</ListGroup>
);
}
Here the localStorage setup for clarity:
import { useEffect, useState } from 'react'
const PREFIX = 'whatsapp-clone-'
export default function useLocalStorage(key, initialValue) {
const prefixedKey = PREFIX + key;
const [value, setValue] = useState(() => {
const jsonValue = localStorage.getItem(prefixedKey);
if (jsonValue != null) return JSON.parse(jsonValue);
if (typeof initialValue === "function") {
return initialValue();
} else {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(prefixedKey, JSON.stringify(value));
}, [prefixedKey, value]);
return [value, setValue];
}
I have been trying to solve this problem for hours and running out of ideas. I setup a test.js and imported the hooks in a Test.js function. Both return their objects respectively.
I have created the application using npx create-react-app and running it via yarn.
The problem lies within your ConversationsProvider.js:
const value = {
conversations: formattedConversations,
createConversation,
};
return (
<ConversationsContext.Provider value={{ value }}>
{children}
</ConversationsContext.Provider>
);
The following lines are all the same:
<ConversationsContext.Provider value={{ value }}>
<ConversationsContext.Provider value={{ value: value }}>
<ConversationsContext.Provider value={{ value: { conversations: formattedConversations, createConversation: createConversation } }}>
When you execute:
const { conversations } = useConversations();
conversations will be set to undefined because the object returned will only have the property value.
To fix the issue remove the nesting by changing the line:
<ConversationsContext.Provider value={{ value }}>
// to
<ConversationsContext.Provider value={value}>
PS. Here a tip to improve your debugging skills. You already did:
const { conversations } = useConversations();
console.log(conversations);
Which logged undefined. The next step would be to not destruct the object immediately and log the whole context value instead.
const contextValue = useConversations();
console.log(contextValue);
This would have shown that the object returned by useConversations is not what you expect it to be.

Categories

Resources