I am trying to create a simple react app Using MobX, TypeScript and React Hooks, where I will type a name of a city, and on clicking an add button, it will update the list of the cities in the UI with the newly added city.
CodeSandbox demo here
The problem is, the list is not getting updated when i click the add button with a new name of a city.
Please help
There are two problems with your code:
Don't use useLocalStore (context.tsx) for creating the store, create the store by directly calling the store function, and wrap the return value of createStore function (store.ts) in an observable
// context.tsx
// const store = useLocalStore(createStore);
const store = createStore()
// store.ts
import { observable } from "mobx";
export const createStore = () => {
const store = observable({
Cities: ["Gotham"],
get allCities(): Array<string> {
return store.Cities;
},
get cityCount(): Number {
return store.Cities.length;
},
addCity: (city: string) => {
console.log("store: adding city: " + city);
store.Cities.push(city);
console.log(store.Cities);
}
});
return store;
};
export type TStore = ReturnType<typeof createStore>;
The return value of your CityView component needs to be wrapped in the useObserver function from mobx-react-lite
// city.tsx
import { useObserver } from "mobx-react-lite";
export const CityView: React.FC<{ cities: string[] }> = ({ cities }) => {
return useObserver(() => {
return (
<ul>
{cities.map((city: string) => (
<li key={city}>{city}</li>
))}
</ul>
);
});
};
code sandbox
Related
Good morning,
I have been struggling with this Next.JS problem during the week, because we have a list of objects in a component that comes from the value of an array in a context.
Introduction
The issue here is:
That we have setSessionList that is a setter for a React state in the context.
sessionList that is an array of objects in the context
and the component InspectionLongList, that is the list itself, where all this objects are rendered and structured.
Question and doubts
Is it possible to conditionally update InspectionLongList when other component of the app updates a value of an object in the array sessionList, using setSessionList from the context?
Which is the good approach to do this?
Some code (for context)
The context:
import { createContext, useContext, useState } from "react";
interface IGobalState {
setSessionList: (value: any) => void;
sessionList: any;
}
export const AppContext = createContext({} as IGobalState);
export function AppWrapper({ children }: any) {
const [sessionList, setSessionList] = useState<any>([]);
return (
<AppContext.Provider
value={{
sessionList,
setSessionList,
}}
>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
And the consumer (wrapped in the Provider)
const {setSessionList, sessionList} = useAppContext();
useEffect(() => {
if (props.auth.isLoggedIn === true){
apiService.getAllInspections(props.auth.user.accessToken).then(res => setSessionList(res))
}
}, [sessionList, setSessionList])
UPDATE: How we consume the context for updates (InspectionLongList)
useEffect(() => {
if (props.auth.isLoggedIn === true){
apiService.getAllInspections(props.auth.user.accessToken).then(res => setSessionList(res))
}
}, [JSON.stringify(sessionList)]);
Thanks in advance,
Carles
I'm trying to map this array but the it keeps saying .includes is undefined. I assume maybe its because I've mapped is wrong? I'm not sure but I will list everything I have.
Here is what my api data looks like when I console.log(this.props.tournaments it from from my redux store:
console.log
You see the undefined in the first index of the array, I've used an Object.keys(tournaments).map(key => tournaments[key]) and .map (which you can see in the component snippet below) twice which when I console.log I get the correct results but I still get an error when its passes through the .filter function.
Here is what it looks like after I've formatted the array:
console.log after formatting
but I'm still getting an error...
This the error I'm getting:
Error message
Here is the component in question:
import React from 'react';
import { connect } from 'react-redux';
import { fetchTournaments } from '../actions/tournaments';
class Search extends React.PureComponent {
// State only needs to hold the current filter text value:
componentDidMount() {
this.props.fetchTournaments();
}
state = {
filterText: ''
};
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
let tourns = this.props.tournaments.map(tore =>
tore.map(room => room.name)
);
console.log(tourns, 'here');
const filteredList = tourns.filter(item =>
item.name.includes(this.state.filterText)
);
return (
<React.Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.name}</li>)}</ul>
</React.Fragment>
);
}
}
function mapStateToProps({ tournaments }) {
return {
tournaments: Object.keys(tournaments).map(key => tournaments[key])
};
}
export default connect(mapStateToProps, {
fetchTournaments
})(Search);
My data is coming from redux like so:
Reducer:
import _ from 'lodash';
import {
FETCH_TOURNAMENT,
from '../actions/types';
export default (state = {}, action) => {
switch (action.type) {
case FETCH_TOURNAMENT:
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
action:
import {
FETCH_TOURNAMENT,
} from './types';
import { API_TOURNAMENTS_URL } from '../constants/api';
import axios from 'axios';
export const fetchTournament = id => async dispatch => {
const response = await axios.get(`http://localhost:4000/tournaments/${id}`);
dispatch({ type: FETCH_TOURNAMENT, payload: response.data });
};
I think you could treat you response.data before setting to state to reflect more how you will be handling your application. I would imagine that you would rather have an array of objects.
you could try passing to payload Object.values(response.data).flat() instead which would give you an array of of objects based on your response.
Edit
below is without implementing Object.values(response.data).flat(). if you follow the suggestion you shouldnt need to treat
about the issue at filter, item is an array containing name value at index 0. this is why you get undefined. this happens because of tore.map(room => room.name) returns an array.
if you change as below you may get your array of tourns:
let tourns = this.props.tournaments.flat();
but I would consider to treat your response which would avoid all these changes on component level. pick one which suits you better.
The following example is pretty simple. When new Book(id); is called, the Book constructor will make an API call to my backend API, get the JSON data for the book, and set the properties on Book like title, synopsis, etc.
The <BookDetailComponent /> is simply supposed to render details of the Book instance.
import React from "react";
import { useParams } from "react-router-dom";
import Book from "../../../modules/books";
export default function BookDetail(props) {
let {id} = useParams();
let book = new Book(id);
return (
<BookDetailComponent book={book} />
)
}
export class BookDetailComponent extends React.Component {
render () {
return (
<div>
<h2>{this.props.book.title}</h2>
<div>
{this.props.book.synopsis}
</div>
</div>
)
}
}
I can see that the data has been received by BookDetailComponent in the React devtools tab. As you can see in the screenshot, props.book is an object with a title, synopsis, etc.
However, none of this information appears to render on the page.
EDIT:
As requested, here's the code for Book:
export default class Book {
constructor(id) {
this.id = id
}
load = (callback) => {
Axios.get(`${process.env.REACT_APP_API_URL}/books/${this.id}`).then(response => {
this.author_id = response.data.author_id
this.title = response.data.title
this.synopsis = response.data.synopsis
})
}
}
Your Book module makes an async call when load function is called. Now it does't trigger a re-render for the BookDetail component to reflect the updated values. Also you cannot directly call the async api in render as of today
I suggest you convert Book into a custom hook that fetches data and use it
const useBook = (id) => {
const [book, setBook] = useState({});
useEffect(() => {
Axios.get(`${process.env.REACT_APP_API_URL}/books/${this.id}`).then(response => {
setBook({
author_id: response.data.author_id,
title: response.data.title,
synopsis: response.data.synopsis,
});
}, [id]);
return book
}
export default useBook;
import useBook from "../../../modules/books";
export default function BookDetail(props) {
let {id} = useParams();
let book = useBook(id);
return (
<BookDetailComponent book={book} />
)
}
My code is here: https://stackblitz.com/edit/react-8g3p3l
There are 2 dumb components:
insertion_citylist.jsx with the following code:
insertion_shoplist.jsx with the following code:
I expect the change on the selected city from the inseriton_citylist.jsx will trigger change on the shop list in insertion_shoplist.jsx. So my container component, app.js contains a function findShops, where the action shoplist(city) is called.
My container component code, app.jsx, is the following:
import React, { Component } from "react";
import { connect } from "react-redux";
import { citylist, shoplist } from "../actions";
import { bindActionCreators } from "redux";
import CityList from "../components/insertion_citylist";
import ShopList from "../components/insertion_shoplist";
class App extends Component {
componentDidMount() {
console.log("mount");
this.props.citylist();
this.props.shoplist();
}
findShops(city) {
console.log("findShops:", city);
this.props.shoplist(city);
}
/* renderShops = shops =>
shops
? shops.map(shop => <option key={shop.id} value={shop.name} />)
: null; */
render() {
console.log("render:" + this.props);
return (
<div>
<CityList data={this.props.data} findShops={this.findShops.bind(this)} />
<ShopList {...this.props} />
{/* <input list="shop" placeholder="shop" />
<datalist id="shop">
{this.renderShops(this.props.shop_data.shops)}
</datalist> */}
</div>
);
}
}
const mapStateToProps = state => {
console.log("map state to props:" + state.shops);
return { data: state.insertion_data }; //Use of spread operator: https://www.youtube.com/watch?v=NCwa_xi0Uuc&t=1721s
//return { city_data: state.cities, shop_data: state.shops };
};
const mapDispathToProps = dispatch => {
console.log("map dispatch to props");
return bindActionCreators({ citylist, shoplist }, dispatch);
};
export default connect(
mapStateToProps,
mapDispathToProps
)(App);
The `findShops` can be called successfully when the city from the downdown list is changed, but seems that the `this.props.shoplist()` just called the action without calling the *reducer*. And the actions code from `actions/index.js` looks like this:
export function citylist() {
return { type: "CITY_LIST", payload: [{city:"Berlin"}, {city: "Frankfurt"}] };
}
export function shoplist(city) {
let data = [{name:"original"},{name:"original 2"}];
if (city === 'Frankfurt') data=[{name:"new"},{name:"new 2"}];
return {
type: "SHOP_LIST",
payload: data
};
}
Problem: The current code is able to trigger the event handler findShops(), but does not succeed in changing the city list state and thus the city list on the insertion_citylist.jsx just keeps unchanged all the while. Could anyone help on this?
Thanks in advance!
it turns out very simple issue
onChange={(e) => props.findShops(e)}
here you are passing e as paramenter
findShops(city) {
this.props.shoplist(city);
}
and using it directly, this is invalid as you will have event in your city parameter.
you need e.target.value
change
<select name="city"
onChange={(e) => props.findShops(e)}>{renderCities(props.data.cities)}</select>
to
<select name="city"
onChange={(e) => props.findShops(e.target.value)}>{renderCities(props.data.cities)}</select>
Demo,
remember react pass an event when you change the value, you need to extract the value when you are reading from it. like you are doing in javascript or jquery.
for checkboxes its e.target.checked and for inputs its e.target.value.
I am quite new to development with React and I am currently trying to get my head around some basic react and redux things. Unfortunately I am experiencing an issue which I cannot fix on my own.
I have written a mock-api which returns players (profileUrl, username, realname, id). I am dispatching an action which successfully gets me one of those players and I can also pass it to my components props using redux' mapStateToPropsfunction. But I cannot render any of that data in my render function. The react devtools even show me that the single player is getting returned as an array.
The component:
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as playerActions from '../../actions/playerActions';
class SinglePlayer extends React.Component {
componentDidMount() {
this.props.actions.loadPlayer(this.props.match.params.playerid);
}
/**
* Render the component.
*/
render() {
return (
<div>
{ this.props.currentPlayer.username }
</div>
)
}
}
/**
* Defines the state which is exposed to this component.
*
* #param { object } reduxStore The entire redux store.
* #param { object } ownProps The properties which belong to the component.
*/
const mapStateToProps = (reduxStore, ownProps) => {
return {
currentPlayer: reduxStore.playerReducer.currentPlayer
}
}
/**
* Defines which actions are exposed to this component.
*
* #param { function } dispatch This function is used to dispatch actions.
*/
const mapDispatchToProps = (dispatch) => {
return {
actions: bindActionCreators(playerActions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SinglePlayer);
React DevTools:
Screenshot of React Devtools props
Redux DevTools:
Screenshot of Redux Devtools data
As you can tell from the image above, the currentPlayer props is inside the playerReducer object.
I have also tried looping over the array like so, with no success either. I just get the error-message stating that .map() is not a function.
this.props.currentPlayer.map(function(player, index) {
return <p>{ player.username }</p>
)}
Error when using .map():
TypeError: this.props.currentPlayer.map is not a function
Can someone tell me what I am doing wrong?
You set your current player by params id at componentDidMount . Your render takes place before that currentPlayer is set hence the error. Add a recheck in your render like below.
render() {
return (
<div>
{
this.props.currentPlayer &&
this.props.currentPlayer.map(function(player, index) {
return <p>{ player.username }</>
)}
}
</div>
)
}
Or
render() {
return (
<div>
{
this.props.currentPlayer ?
this.props.currentPlayer.map(function(player, index) {
return <p>{ player.username }</>
)}
:
null
}
</div>
)
}
Either way it should work. That way this.props.currentPlayer will not be rendered or accessed until its available.
Update
Udate your mapStateToProps to
const mapStateToProps = (state) => {
return {
currentPlayer: state.currentPlayer
}
}
I think from your reduxDev tool, currentPlayer is not under any object.
in first render this.props.currentPlayer is empty!
set empty array "currentPlayer" in state and insert insert this.props.currentPlayer in this.state.currentPlayer and render from state
I managed to solve the issue myself now. The posts here kinda inspired me to try some new things. It was my mock-api which returned the data in a strange and unexpected (at least for me it was unexpected) way.
The dataset:
const players = [
{
profileUrl: 'https://profile.url',
username: 'player1',
realname: 'Max Muster',
steamId: 'player1'
},
{
profileUrl: 'https://profile.url',
username: 'player2',
realname: 'Max Mustermann',
steamId: 'player2'
},
{
profileUrl: 'https://profile.url',
username: 'player3',
realname: 'Maxime Musterfrau',
steamId: 'player3'
},
];
The filtermethod used:
var player = players.filter(function(el) {
return el.steamId === 'player1';
});
If you assume an array of multiple players like above, the shown filtermethod extracts the correct object but still keeps it wrapped in an array. That was producing the mistake...
Thanks a lot for the help guys!