I have a very large app on mobx + react, where many components call the same actions on store. For example, "delete photo", can be call from a list of photos or from a modal. But before execute the action I have, for example, to show a confirmation modal...
I end-up with this soluction, but it looks like that I'm mixin data login with view logic...
class PhotoStore {
#observable photos;
#action destroy(photo) {
if (currentUser.isGuest) {
modalStore.open('NoGuest')
return
}
modalStore.openConfirm(() => {
// some datalogic
api.delete('/photos/'+photo.id).then(() => {
notificationStore.showSuccess('your photo was deleted!')
})
})
}
}
const PhotoView = observer(({photo}) => {
return <div onClick={() => photoStore.destroy(photo)}>...</div>
})
What so you thing? is that ok to go?
thanks a lot!
To avoid UI logic with model logic, the code can be simplified as follows.
class PhotoView extends React.Component {
handleDelete() {
if (this.props.currentUser.isGuest) {
modalStore.open('NoGuest');
return;
}
modalStore.openConfirm(() => {
// some datalogic
photoStore.delete(this.props.photo).then(() => {
notificationStore.showSuccess('your photo was deleted!');
});
});
}
render() {
return <div onClick={this.handleDelete.bind(this)}>...</div>
}
}
The delete function on the PhotoStore should be modified:
#action delete(photo) {
return new Promise((res, rej) => {
api.delete('/photos/'+photo.id)
.then(res)
.catch(rej);
});
}
Instead of
notificationStore.showSuccess('your photo was deleted!')
You could add an observable to store and modify it there. Your modal code could live somewhere else and be an observer
Related
I can't get this to work correctly after several hours.
When creating a component that needs data from Firebase to display, the data is returning after all actions have taken place so my component isn't showing until pressing the button again which renders again and shows correctly.
Currently my function is finishing before setState, and setState is happening before the data returns.
I can get setState to happen when the data is returned by using the callback on setState but the component would have already rendered.
How do i get the component to render after the data has returned?
Or what would the correct approach be?
class CoffeeList extends Component {
constructor(props) {
super(props);
this.state = {
coffeeList: [],
}
}
componentDidMount() {
this.GetCoffeeList()
}
GetCoffeeList() {
var cups = []
coffeeCollection.get().then((querySnapshot) => {
querySnapshot.forEach(function (doc) {
cups.push({ name: doc.id})
});
console.log('Updating state')
console.log(cups)
})
this.setState({ coffeeList: cups })
console.log('End GetCoffeeList')
}
render() {
const coffeeCups = this.state.coffeeList;
console.log("Rendering component")
return (
<div className="coffee">
<p> This is the Coffee Component</p>
{coffeeCups.map((c) => {
return (
<CoffeeBox name={c.name} />
)
})}
</div >
)
}
}
Thanks
The problem is that you set the state before the promise is resolved. Change the code in the following way:
GetCoffeeList() {
coffeeCollection.get().then((querySnapshot) => {
const cups = []
querySnapshot.forEach(function (doc) {
cups.push({ name: doc.id})
});
console.log('Updating state')
console.log(cups)
this.setState({ coffeeList: cups })
console.log('End GetCoffeeList')
})
}
Hi I've been trying out a bit of react and electron and I'm just trying to make a media playlist type of application but I am having a bit of trouble with Redux
So I have setup the actions and they do kind of work but for some reason on the first initial load of a component the array.map that I am using to display all the results in a list won't actually render.
I have got the console out putting the results and when the component renders the first pass of the render fucntion the initial state is null then on the second pass the console logs the correct output but the array.map is still not outputting anything.
Then when I save a file in my editor (hot reload is on) I will then get an item to render on from the array.map
I can't work out if I have made astupid mistake somewhere or if I am just completly doing the wrong thing. So I'm hoping maybe someone might be able to shed some light on the situation.
Here is my component file with this array.map function that isn't working
interface Props {
songs?: any;
receiveMedia?: Function;
}
interface SongState {}
export default class Songs extends React.Component<Props, SongState> {
private initLoad = 0;
constructor(props: Props) {
super(props);
this.state = {};
this.getThumbnailRender = this.getThumbnailRender.bind(this);
}
componentDidMount() {
this.props.receiveMedia && this.props.receiveMedia();
}
getThumbnailRender() {
console.log(this.props.songs);
if (this.props.songs.Media.songs !== null) {
return this.props.songs.Media.songs.map((media: any) => {
if (media.extension !== "mp4") {
return (
<li
className={css.thumbNail}
id={media.id}
key={`${"media_thumb_"}${media.id}`}
>
<img src={mp3} />
<span className={css.floatingText}>{media.fileName}</span>
</li>
);
} else {
return (
<li
className={css.thumbNail}
id={media.id}
key={`${"media_thumb_"}${media.id}`}
>
<img src={media.filePath} />
<span className={css.floatingText}>{media.fileName}</span>
</li>
);
}
});
}
return <div>You haven't added any songs</div>;
}
render() {
return (
<div className={css.container}>
<h1>Songs</h1>
<div className={css.songHolder}>
<ul>{this.getThumbnailRender()}</ul>
</div>
</div>
);
}
}
I'm pretty sure that the Action and the reducer files are fine as they work later on but I will include them just incase I have made a stupid mistake
ACTIONS.ts
import { Songs } from "../../Models";
var fs = require("fs");
export enum ActionTypes {
MEDIA_RECEIVED = "[Media] MEDIA_RECEIVED"
}
export interface MediaReceived {
type: ActionTypes.MEDIA_RECEIVED;
payload: {
globals: Songs;
};
}
export function mediaReceived(json: any): MediaReceived {
return {
type: ActionTypes.MEDIA_RECEIVED,
payload: {
globals: json
}
};
}
function loadInCurrentSongList() {
var obj: Songs = {
//#ts-ignore
songs: []
};
fs.readFile("saveFile.json", "utf-8", (err: any, data: any) => {
if (err) {
alert("An error ocurred reading the file :" + err.message);
return;
}
const newData = JSON.parse(data);
if (newData.songs.length > 0) {
newData.songs.map((song: any) => {
obj.songs.push(song);
});
}
});
return obj;
}
export function receiveMedia() {
return (dispatch: Function) => {
dispatch(mediaReceived(loadInCurrentSongList()));
};
}
export type Action = MediaReceived;
REDUCER.ts
import { Songs } from "../../Models";
import { Action, ActionTypes } from "./Actions";
export interface State {
songs: Songs | null;
}
export const initialState: State = {
songs: null
};
export function reducer(state: State = initialState, action: Action) {
if (action.type === ActionTypes.MEDIA_RECEIVED) {
return Object.assign({}, state, action.payload.globals);
} else {
return state;
}
}
Thank you very much :)
Because fs.readFile is async and the callback passed to it is executed at a later time when the fs operation completes, obj is being returned from loadInCurrentSongList before it is populated with the songs, and therefore when mediaReceived is dispatched songs is still empty. Your debugger is fooling you a bit because it displays the updated value of obj after it gets populated in the fs.readFile callback.
The hot reload works because it forces a re-render without destroying state, at which point obj has been mutated inside of the fs.readFile callback.
Here's one option to manage the async nature of fs.readFile with a Promise such that you wait for it complete instead of mutating the obj returned from loanInCurrentSongList. Not super familiar with typescript so you'll have to update the types probably:
function loadInCurrentSongList() {
return new Promise(resolve => {
fs.readFile("saveFile.json", "utf-8", (err: any, data: any) => {
var obj: Songs = {
//#ts-ignore
songs: []
};
if (err) {
alert("An error ocurred reading the file :" + err.message);
resolve(obj);
return;
}
const newData = JSON.parse(data);
if (newData.songs.length > 0) {
newData.songs.map((song: any) => {
obj.songs.push(song);
});
}
resolve(obj);
});
}
export function receiveMedia() {
return (dispatch: Function) => {
loadInCurrentSongList().then(songList => {
dispatch(mediaReceived(songList));
}
};
}
I am trying to add sorting to my movie app, I had a code that was working fine but there was too much code repetition, I would like to take a different approach and keep my code DRY. Anyways, I am confused as on which method should I set the state when I make my AJAX call and update it with a click event.
This is a module to get the data that I need for my app.
export const moviesData = {
popular_movies: [],
top_movies: [],
theaters_movies: []
};
export const queries = {
popular:
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=###&page=",
top_rated:
"https://api.themoviedb.org/3/movie/top_rated?api_key=###&page=",
theaters:
"https://api.themoviedb.org/3/movie/now_playing?api_key=###&page="
};
export const key = "68f7e49d39fd0c0a1dd9bd094d9a8c75";
export function getData(arr, str) {
for (let i = 1; i < 11; i++) {
moviesData[arr].push(str + i);
}
}
The stateful component:
class App extends Component {
state = {
movies = [],
sortMovies: "popular_movies",
query: queries.popular,
sortValue: "Popularity"
}
}
// Here I am making the http request, documentation says
// this is a good place to load data from an end point
async componentDidMount() {
const { sortMovies, query } = this.state;
getData(sortMovies, query);
const data = await Promise.all(
moviesData[sortMovies].map(async movie => await axios.get(movie))
);
const movies = [].concat.apply([], data.map(movie => movie.data.results));
this.setState({ movies });
}
In my app I have a dropdown menu where you can sort movies by popularity, rating, etc. I have a method that when I select one of the options from the dropwdown, I update some of the states properties:
handleSortValue = value => {
let { sortMovies, query } = this.state;
if (value === "Top Rated") {
sortMovies = "top_movies";
query = queries.top_rated;
} else if (value === "Now Playing") {
sortMovies = "theaters_movies";
query = queries.theaters;
} else {
sortMovies = "popular_movies";
query = queries.popular;
}
this.setState({ sortMovies, query, sortValue: value });
};
Now, this method works and it is changing the properties in the state, but my components are not re-rendering. I still see the movies sorted by popularity since that is the original setup in the state (sortMovies), nothing is updating.
I know this is happening because I set the state of movies in the componentDidMount method, but I need data to be Initialized by default, so I don't know where else I should do this if not in this method.
I hope that I made myself clear of what I am trying to do here, if not please ask, I'm stuck here and any help is greatly appreciated. Thanks in advance.
The best lifecycle method for fetching data is componentDidMount(). According to React docs:
Where in the component lifecycle should I make an AJAX call?
You should populate data with AJAX calls in the componentDidMount() lifecycle method. This is so you can use setState() to update your component when the data is retrieved.
Example code from the docs:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("https://api.example.com/items")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.items
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<ul>
{items.map(item => (
<li key={item.name}>
{item.name} {item.price}
</li>
))}
</ul>
);
}
}
}
Bonus: setState() inside componentDidMount() is considered an anti-pattern. Only use this pattern when fetching data/measuring DOM nodes.
Further reading:
HashNode discussion
StackOverflow question
I have the following code which retrieves Google Places Reviews based on Google Places API. I have incorporated the logic to work as a React life cycle component. Currently, I am unable to setState and correctly bind the object. I could use some help understanding where my logic is failing.
export default class Reviews extends React.Component{
constructor(props){
super(props);
this.state = {
places: []
}
}
componentDidMount(){
let map = new google.maps.Map(document.getElementById("map"), {
center: {lat:40.7575285, lng: -73.9884469}
});
let service = new google.maps.places.PlacesService(map);
service.getDetails({
placeId: 'ChIJAUKRDWz2wokRxngAavG2TD8'
}, function(place, status) {
if (status === google.maps.places.PlacesServiceStatus.OK) {
console.log(place.reviews);
// Intended behavior is to set this.setState({places.place.reviews})
}
})
}
render(){
const { places } = this.state;
return(
<div>
<p>
{
places.map((place) => {
return <p>{place.author_name}{place.rating}{place.text}</p>
})
}
</p>
</div>
)
}
}
You can't use this that way in a callback. When the function is called the this in, this.setState({places.place.reviews}) doesn't point to your object. One solution is to use => function notation which will bind this lexically.
service.getDetails({
placeId: 'ChIJAUKRDWz2wokRxngAavG2TD8'
}, (place, status) => {
if (status === google.maps.places.PlacesServiceStatus.OK) {
console.log(place.reviews);
this.setState({places: place.reviews})
}
})
}
Alternatively you can make a new reference to this and us it in the function. Something like
var that = this
...
that({places.place.reviews})
The first option is nicer, but requires an environment where you can use ES6. Since your using let you probably are okay.
With some tweaking -- I got the code to work! Thank you.
render(){
const { places } = this.state;
return(
<div>
<p>
{
places.map((place) => {
if(place.rating >= 4){
return <p key={place.author_name}>{place.author_name}{place.rating}{place.text}</p>
}
})
}
</p>
</div>
)
}
I emitted the data from MerchantComponent
and subscribed via EventEmitterService from MerchantPaymentChannelComponent, its OK when route directly opens this page. But you see there is other tabs which every tab has its own components. When I change the tab to different one, then come back to this MerchantPaymentChannelComponent its not subscribe again.
NOTE: I'm doing unsubscription on NgOnDestroy event
Here is codes;
MerchantListDetailService (SHARING SERVICE VIA EVENTEMITTER)
export class MerchantListDetailService {
#Output() emittedMerchant: EventEmitter<any> = new EventEmitter();
constructor() {}
emitMerchant(data) {
this.emittedMerchant.emit(data);
}
getEmittedValue() {
return this.emittedMerchant;
}
}
MerchantComponent (TRIGGERING EMIT FROM THIS COMPONENT)
private getMerchantDetail() {
let data = {
Id: this.merchantId,
}
this.merchantsService.getMerchants(data)
.then((res) => {
// console.log(res)
if (res.Success) {
this.merchant = res.Data[0];
this.merchantListDetailService.emitMerchant(res.Data[0]);
}
})
.catch((err) => { })
}
MerchantPaymentChannelComponent (SUBSCRIBING FROM THIS COMPONENT)
ngOnInit() {
this.merchantSubscribe = this.merchantListDetailService.getEmittedValue()
.subscribe(merchant => {
this.merchant = merchant;
this.getMerchantPaymentChannels();
})
}
ngOnDestroy() {
this.merchantSubscribe.unsubscribe();
}