Right now I am trying to console.log this.streamCreatorUid, but I'm running into a peculiar issue. In my redux debugger, I can clearly see my data in the proper place.
Here is my redux data for the stream creator, directly from my debugger.
streams -
[0] -
{category(pin): "Oldschool Runescape"
displayName(pin): "admin"
streamId(pin): "98ebc719-c7d5-4558-b99d-2d9f8306ec64"
title(pin): "accounttest"
uid(pin): "wsFc7pIMq5dMtw9hPU86DzUTdLO2"
}
I am trying to console.log this.streamCreatorUid from my mapstatetoprops, but it is returning the current user Uid of u9TcrICehNMlAmqyDHQY77L9CXq1 instead. I'm quite confused as to why this is happening, considering this is not the data shown in my debugger.
This is for a personal project. In the past I've accessed redux props like this with no issues, now I'm not quite sure why this is happening.
import React from 'react';
import { database } from '../../../firebaseconfig.js';
import { connect } from 'react-redux';
class StreamFollow extends React.Component {
constructor(props) {
super(props);
this.uid = this.props.uid;
this.displayName = this.props.displayName;
this.streamCreatorUid = this.props.streamCreatorUid;
this.streamCreatorDisplayName = this.props.streamCreatorDisplayName;
}
componentShouldUpdate(prevProps) {
if (this.props.uid !== prevProps.uid) {
this.uid = this.props.uid
}
if (this.props.streamCreatorUid !== prevProps.streamCreatorUid) {
this.streamCreatorUid = this.props.streamCreatorUid;
}
}
//creates a follower object under the stream creators uid
createFollower = (e) => {
const followerRef = database.ref(`User_Follow_Info/${e}/Follower`)
const followerInfoObject = {
uid: this.uid,
displayName: this.displayName
}
followerRef.push(followerInfoObject);
}
//creates a following object under the users uid
//Add in checks to see if following object already exists. We can't follow someone multiple times
createFollowing = (user) => {
const followingRef = database.ref(`User_Follow_Info/${user}/Following`);
const followingInfoObject = {
uid: this.streamCreatorUid,
displayName: this.streamCreatorDisplayName
}
console.log(this.streamCreatorDisplayName)
//Check to see if follow already exists.
/*followingRef.once('value', function (snapshot) {
if (snapshot.hasChild(DATA HERE)) {
alert('exists');
}
}); */
var isSignedIn = this.isSignedIn;
followingRef.orderByChild('uid').equalTo(this.uid).once('value').then(snapshot => {
console.log(snapshot.val());
console.log(this.streamCreatorUid);
if (isSignedIn) {
console.log(snapshot.val())
return
} else {
followingRef.push(followingInfoObject);
}
})
}
onSubmit = () => {
if (this.props.isSignedIn === true) {
this.createFollowing(this.uid);
this.createFollower(this.streamCreatorUid);
} else {
//add in a sign in modal if user is not logged in
console.log('please sign in')
}
}
render() {
return (
<div>
<button onClick={this.onSubmit}>Follow</button>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
isSignedIn: state.auth.isSignedIn,
displayName: state.auth && state.auth.userInfo ? state.auth.userInfo.displayName : null,
uid: state.auth && state.auth.userInfo ? state.auth.userInfo.uid : null,
streamCreatorUid: state.streams && state.streams[0] ? state.streams[0].uid : null,
streamCreatorDisplayName: state.streams && state.streams[0] ? state.streams[0].displayName : null,
}
}
export default connect(mapStateToProps)(StreamFollow);
Related
I'm learning React and as a practice I'm building a weather-app.
My App Photo
Everything is ok. but I want When a User type a City name for example 'London' and click the Search button and get the data . the next search if it's 'london' (which is currently visible) i don't want my handleGetData function to call server for data.
How should implement this logic ? What should i change? I will appreciate your help
import React, { Component } from "react";
import InputSearch from "./common/inputSearch";
import http from "../services/httpService";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import Weather from "./weather";
class App extends Component {
state = {
city: {
name: "",
info: [],
},
};
handleChange = (e) => {
const city = this.state.city;
city.name = e.currentTarget.value;
this.setState({ city });
};
handleGetData = async () => {
try {
const city = this.state.city;
const apiEndPoint = `http://api.weatherapi.com/v1/forecast.json?key=97de37820e0e4a29b9c90051221604&q=${city.name}&days=3&aqi=no&alerts=no`;
const { data } = await http.get(apiEndPoint);
city.info = data;
this.setState({ city });
} catch (e) {
const expectedError =
e.response.status && e.response.status > 400 && e.response.status < 500;
if (expectedError) {
toast.error("Something failed while getting Data");
}
}
};
render() {
const { city } = this.state;
return (
<React.Fragment>
<ToastContainer />
<InputSearch
stateCityName={city.name}
onChanges={this.handleChange}
onGettingData={this.handleGetData}
/>
<Weather data={city.info} data2={city} />
</React.Fragment>
);
}
}
export default App;
You can use a new state as your previousSearch and compare it with your current search
It's something like this but I didn't test it though so you may have to do some modification
state = {
city: {
name: "",
info: [],
},
previousSearch: "",
};
handleChange = (e) => {
const city = this.state.city;
prevCity = city.name;
city.name = e.currentTarget.value;
this.setState({ city, previousSearch: prevCity });
};
handleGetData = async () => {
try {
const city = this.state.city;
if(previousSearch !== city.name) {
const apiEndPoint = `http://api.weatherapi.com/v1/forecast.json?key=97de37820e0e4a29b9c90051221604&q=${city.name}&days=3&aqi=no&alerts=no`;
const { data } = await http.get(apiEndPoint);
city.info = data;
this.setState({ city });
}
} catch (e) {
const expectedError =
e.response.status && e.response.status > 400 && e.response.status < 500;
if (expectedError) {
toast.error("Something failed while getting Data");
}
}
};
Have fun
I'm building a simple note-taking app and I'm trying to add new note at the end of the list of notes, and then see the added note immediately. Unfortunately I'm only able to do it by refreshing the page. Is there an easier way?
I know that changing state would usually help, but I have two separate components and I don't know how to connect them in any way.
So in the NewNoteForm component I have this submit action:
doSubmit = async () => {
await saveNote(this.state.data);
};
And then in the main component I simply pass the NewNoteForm component.
Here's the whole NewNoteForm component:
import React from "react";
import Joi from "joi-browser";
import Form from "./common/form";
import { getNote, saveNote } from "../services/noteService";
import { getFolders } from "../services/folderService";
class NewNoteForm extends Form {
//extends Form to get validation and handling
state = {
data: {
title: "default title",
content: "jasjdhajhdjshdjahjahdjh",
folderId: "5d6131ad65ee332060bfd9ea"
},
folders: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string().label("Title"),
content: Joi.string()
.required()
.label("Note"),
folderId: Joi.string()
.required()
.label("Folder")
};
async populateFolders() {
const { data: folders } = await getFolders();
this.setState({ folders });
}
async populateNote() {
try {
const noteId = this.props.match.params.id;
if (noteId === "new") return;
const { data: note } = await getNote(noteId);
this.setState({ data: this.mapToViewModel(note) });
} catch (ex) {
if (ex.response && ex.response.status === 404)
this.props.history.replace("/not-found");
}
}
async componentDidMount() {
await this.populateFolders();
await this.populateNote();
}
mapToViewModel(note) {
return {
_id: note._id,
title: note.title,
content: note.content,
folderId: note.folder._id
};
}
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}
doSubmit = async () => {
await saveNote(this.state.data);
};
render() {
return (
<div>
<h1>Add new note</h1>
<form onSubmit={this.handleSubmit}>
{this.renderSelect("folderId", "Folder", this.state.folders)}
{this.renderInput("title", "Title")}
{this.renderInput("content", "Content")}
{this.renderButton("Add")}
</form>
</div>
);
}
}
export default NewNoteForm;
And here's the whole main component:
import React, { Component } from "react";
import { getNotes, deleteNote } from "../services/noteService";
import ListGroup from "./common/listGroup";
import { getFolders } from "../services/folderService";
import { toast } from "react-toastify";
import SingleNote from "./singleNote";
import NewNoteForm from "./newNoteForm";
class Notes extends Component {
state = {
notes: [], //I initialize them here so they are not undefined while componentDidMount is rendering them, otherwise I'd get a runtime error
folders: [],
selectedFolder: null
};
async componentDidMount() {
const { data } = await getFolders();
const folders = [{ _id: "", name: "All notes" }, ...data];
const { data: notes } = await getNotes();
this.setState({ notes, folders });
}
handleDelete = async note => {
const originalNotes = this.state.notes;
const notes = originalNotes.filter(n => n._id !== note._id);
this.setState({ notes });
try {
await deleteNote(note._id);
} catch (ex) {
if (ex.response && ex.response.status === 404)
toast.error("This note has already been deleted.");
this.setState({ notes: originalNotes });
}
};
handleFolderSelect = folder => {
this.setState({ selectedFolder: folder }); //here I say that this is a selected folder
};
render() {
const { selectedFolder, notes } = this.state;
const filteredNotes =
selectedFolder && selectedFolder._id //if the selected folder is truthy I get all the notes with this folder id, otherwise I get all the notes
? notes.filter(n => n.folder._id === selectedFolder._id)
: notes;
return (
<div className="row m-0">
<div className="col-3">
<ListGroup
items={this.state.folders}
selectedItem={this.state.selectedFolder} //here I say that this is a selected folder
onItemSelect={this.handleFolderSelect}
/>
</div>
<div className="col">
<SingleNote
filteredNotes={filteredNotes}
onDelete={this.handleDelete}
/>
<NewNoteForm />
</div>
</div>
);
}
}
export default Notes;
How can I connect these two components so that the data shows smoothly after submitting?
You can use a callback-like pattern to communicate between a child component and its parent (which is the 3rd strategy in #FrankerZ's link)
src: https://medium.com/#thejasonfile/callback-functions-in-react-e822ebede766)
Essentially you pass in a function into the child component (in the main/parent component = "Notes": <NewNoteForm onNewNoteCreated={this.onNewNoteCreated} />
where onNewNoteCreated can accept something like the new note (raw data or the response from the service) as a parameter and saves it into the parent's local state which is in turn consumed by any interested child components, i.e. ListGroup).
Sample onNewNoteCreated implementation:
onNewNoteCreated = (newNote) => {
this.setState({
notes: [...this.state.notes, newNote],
});
}
Sample use in NewNoteForm component:
doSubmit/handleSubmit = async (event) => {
event.preventDefault();
event.stopPropagation();
const newNote = await saveNote(this.state.data);
this.props.onNewNoteCreated(newNote);
}
You probably want to stop the refresh of the page on form submit with event.preventDefault() and event.stopPropagation() inside your submit handler (What's the difference between event.stopPropagation and event.preventDefault?).
I am getting the following error,
"Uncaught TypeError: Cannot read property 'setState' of undefined
at Socket.eval (CommunicationContainer.js?41c1:40)
at Socket.Emitter.emit (index.js?7297:133)
at Socket.onevent (socket.js?2851:278)
at Socket.onpacket (socket.js?2851:236)
at Manager.eval (index.js?40de:21)
at Manager.Emitter.emit (index.js?7297:133)
at Manager.ondecoded (manager.js?78eb:345)
at Decoder.eval (index.js?40de:21)
at Decoder.Emitter.emit (index.js?7297:133)
at Decoder.add (index.js?568d:251)
"
Through all I've looked up this is because the binding is not properly done inside the constructor, however after everything I've done as far as I can tell I have all proper bindings and have no idea why this error is occurring. The error says it occurs in the CommmunicationContainer which I have attached here
Communication Container
import React from 'react'
import { PropTypes } from 'prop-types';
import Remarkable from 'remarkable-react'
import MediaContainer from './MediaContainer'
import Communication from '../components/Communication'
import store from '../store/configureStore'
import { connect } from 'react-redux'
class CommunicationContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
sid: '',
message: '',
audio: true,
video: true
};
this.handleInvitation = this.handleInvitation.bind(this);
this.handleHangup = this.handleHangup.bind(this);
this.handleInput = this.handleInput.bind(this);
this.toggleVideo = this.toggleVideo.bind(this);
this.toggleAudio = this.toggleAudio.bind(this);
this.send = this.send.bind(this);
this.hideAuth = this.hideAuth.bind(this);
this.full = this.full.bind(this);
}
hideAuth() {
this.props.media.setState({bridge: 'connecting'});
}
full() {
this.props.media.setState({bridge: 'full'});
}
componentDidMount() {
console.log('Comments componentDidMount: ');
console.log(this);
const socket = this.props.socket;
console.log('props', this.props)
this.setState({video: this.props.video, audio: this.props.audio});
socket.on('create', () =>
this.props.media.setState({user: 'host', bridge: 'create'}));
socket.on('full', this.full);
socket.on('bridge', role => this.props.media.init());
socket.on('join', () =>
this.props.media.setState({user: 'guest', bridge: 'join'}));
socket.on('approve', ({ message, sid }) => {
this.props.media.setState({bridge: 'approve'});
this.setState({ message, sid });
});
socket.emit('find');
this.props.getUserMedia
.then(stream => {
this.localStream = stream;
this.localStream.getVideoTracks()[0].enabled = this.state.video;
this.localStream.getAudioTracks()[0].enabled = this.state.audio;
});
}
handleInput(e) {
this.setState({[e.target.dataset.ref]: e.target.value});
}
send(e) {
e.preventDefault();
this.props.socket.emit('auth', this.state);
this.hideAuth();
}
handleInvitation(e) {
e.preventDefault();
this.props.socket.emit([e.target.dataset.ref], this.state.sid);
this.hideAuth();
}
getContent(content) {
return {__html: (new Remarkable()).render(content)};
}
toggleVideo() {
const video = this.localStream.getVideoTracks()[0].enabled = !this.state.video;
this.setState({video: video});
this.props.setVideo(video);
}
toggleAudio() {
const audio = this.localStream.getAudioTracks()[0].enabled = !this.state.audio;
this.setState({audio: audio});
this.props.setAudio(audio);
}
handleHangup() {
this.props.media.hangup();
}
render(){
console.log(this.media);
return (
<Communication
{...this.state}
toggleVideo={this.toggleVideo}
toggleAudio={this.toggleAudio}
getContent={this.getContent}
send={this.send}
handleHangup={this.handleHangup}
handleInput={this.handleInput}
handleInvitation={this.handleInvitation} />
);
}
}
const mapStateToProps = store => ({video: store.video, audio: store.audio});
const mapDispatchToProps = dispatch => (
{
setVideo: boo => store.dispatch({type: 'SET_VIDEO', video: boo}),
setAudio: boo => store.dispatch({type: 'SET_AUDIO', audio: boo})
}
);
CommunicationContainer.propTypes = {
socket: PropTypes.object.isRequired,
getUserMedia: PropTypes.object.isRequired,
audio: PropTypes.bool.isRequired,
video: PropTypes.bool.isRequired,
setVideo: PropTypes.func.isRequired,
setAudio: PropTypes.func.isRequired,
media: PropTypes.instanceOf(MediaContainer)
};
export default connect(mapStateToProps, mapDispatchToProps)(CommunicationContainer);
I've included the other Containers and Components for reference in case the error has something to do with them, but as far as is being shown the error is within the CommunicationContainer with the state being undefined at the socket line. As well I have included the error image.
This application is a WebRTC React page within a React website where the user sets the room ID and upon pressing the button launches into the room.
The Error Code
The Error Line
Chat Room Page
import React, { Component } from 'react';
import MediaContainer from './MediaContainer'
import CommunicationContainer from './CommunicationContainer'
import { connect } from 'react-redux'
import store from '../store/configureStore'
import io from 'socket.io-client'
class ChatRoomPage extends React.Component {
constructor(props) {
super(props);
this.getUserMedia = navigator.mediaDevices.getUserMedia({
audio: true,
video: true
}).catch(e => alert('getUserMedia() error: ' + e.name))
this.socket = io.connect('https://localhost:8080');
}
componentDidMount() {
this.props.addRoom();
}
render() {
console.log(this.socket);
console.log(this.getUserMedia);
console.log(this.media);
return (
<div>
<MediaContainer media={media => this.media = media} socket={this.socket} getUserMedia={this.getUserMedia} />
<CommunicationContainer socket={this.socket} media={this.media} getUserMedia={this.getUserMedia} />
<h1>AppointmentSetup</h1>
</div>
);
}
}
//commented out
const mapStateToProps = store => ({rooms: new Set([...store.rooms])});
const mapDispatchToProps = (dispatch, ownProps) => (
{
addRoom: () => store.dispatch({ type: 'ADD_ROOM', room: ownProps.match.params.room })
}
);
export default connect(mapStateToProps, mapDispatchToProps)(ChatRoomPage);
Media Container
import React, { Component } from 'react';
import { PropTypes } from 'prop-types';
class MediaBridge extends Component {
constructor(props) {
super(props);
this.state = {
bridge: '',
user: ''
}
this.onRemoteHangup = this.onRemoteHangup.bind(this);
this.onMessage = this.onMessage.bind(this);
this.sendData = this.sendData.bind(this);
this.setupDataHandlers = this.setupDataHandlers.bind(this);
this.setDescription = this.setDescription.bind(this);
this.sendDescription = this.sendDescription.bind(this);
this.hangup = this.hangup.bind(this);
this.init = this.init.bind(this);
this.setDescription = this.setDescription.bind(this);
}
componentWillMount() {
// chrome polyfill for connection between the local device and a remote peer
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection;
this.props.media(this);
}
componentDidMount() {
this.props.getUserMedia
.then(stream => this.localVideo.srcObject = this.localStream = stream);
this.props.socket.on('message', this.onMessage);
this.props.socket.on('hangup', this.onRemoteHangup);
}
componentWillUnmount() {
this.props.media(null);
if (this.localStream !== undefined) {
this.localStream.getVideoTracks()[0].stop();
}
this.props.socket.emit('leave');
}
onRemoteHangup() {
this.setState({user: 'host', bridge: 'host-hangup'});
}
onMessage(message) {
if (message.type === 'offer') {
// set remote description and answer
this.pc.setRemoteDescription(new RTCSessionDescription(message));
this.pc.createAnswer()
.then(this.setDescription)
.then(this.sendDescription)
.catch(this.handleError); // An error occurred, so handle the failure to connect
} else if (message.type === 'answer') {
// set remote description
this.pc.setRemoteDescription(new RTCSessionDescription(message));
} else if (message.type === 'candidate') {
// add ice candidate
this.pc.addIceCandidate(
new RTCIceCandidate({
sdpMLineIndex: message.mlineindex,
candidate: message.candidate
})
);
}
}
sendData(msg) {
this.dc.send(JSON.stringify(msg))
}
// Set up the data channel message handler
setupDataHandlers() {
this.dc.onmessage = e => {
var msg = JSON.parse(e.data);
console.log('received message over data channel:' + msg);
};
this.dc.onclose = () => {
this.remoteStream.getVideoTracks()[0].stop();
console.log('The Data Channel is Closed');
};
}
setDescription(offer) {
this.pc.setLocalDescription(offer);
}
// send the offer to a server to be forwarded to the other peer
sendDescription() {
this.props.socket.send(this.pc.localDescription);
}
hangup() {
this.setState({user: 'guest', bridge: 'guest-hangup'});
this.pc.close();
this.props.socket.emit('leave');
}
handleError(e) {
console.log(e);
}
init() {
// wait for local media to be ready
const attachMediaIfReady = () => {
this.dc = this.pc.createDataChannel('chat');
this.setupDataHandlers();
console.log('attachMediaIfReady')
this.pc.createOffer()
.then(this.setDescription)
.then(this.sendDescription)
.catch(this.handleError); // An error occurred, so handle the failure to connect
}
// set up the peer connection
// this is one of Google's public STUN servers
// make sure your offer/answer role does not change. If user A does a SLD
// with type=offer initially, it must do that during the whole session
this.pc = new RTCPeerConnection({iceServers: [{url: 'stun:stun.l.google.com:19302'}]});
// when our browser gets a candidate, send it to the peer
this.pc.onicecandidate = e => {
console.log(e, 'onicecandidate');
if (e.candidate) {
this.props.socket.send({
type: 'candidate',
mlineindex: e.candidate.sdpMLineIndex,
candidate: e.candidate.candidate
});
}
};
// when the other side added a media stream, show it on screen
this.pc.onaddstream = e => {
console.log('onaddstream', e)
this.remoteStream = e.stream;
this.remoteVideo.srcObject = this.remoteStream = e.stream;
this.setState({bridge: 'established'});
};
this.pc.ondatachannel = e => {
// data channel
this.dc = e.channel;
this.setupDataHandlers();
this.sendData({
peerMediaStream: {
video: this.localStream.getVideoTracks()[0].enabled
}
});
//sendData('hello');
};
// attach local media to the peer connection
this.localStream.getTracks().forEach(track => this.pc.addTrack(track, this.localStream));
// call if we were the last to connect (to increase
// chances that everything is set up properly at both ends)
if (this.state.user === 'host') {
this.props.getUserMedia.then(attachMediaIfReady);
}
}
render(){
console.log(this.media);
console.log(this.getUserMedia)
return (
<div className={`media-bridge ${this.state.bridge}`}>
<video className="remote-video" ref={(ref) => this.remoteVideo = ref} autoPlay></video>
<video className="local-video" ref={(ref) => this.localVideo = ref} autoPlay muted></video>
</div>
);
}
}
MediaBridge.propTypes = {
socket: PropTypes.object.isRequired,
getUserMedia: PropTypes.object.isRequired,
media: PropTypes.func.isRequired
}
export default MediaBridge;
Any help is greatly appreciated as I don't see any binding issues within this yet continue to get the error no matter what I try.
Those methods have not bin bind on your controller and you are calling them from other methods , it may be the reason!
hideAuth()
full()
I created array of routing in ReactJS
const routes = [
{ id: 0, path: '/', view: Home, parent: 0 },
{ id: 1, path: '/a', view: Home2, parent: 0 },
{ id: 2, path: '/b', view: Home3, parent: 1 }
]
Created HOC withAuth which should back to parent routing when user isn't logged. When i going to route (as not logged) - its ok and withAuth back me to parent route, but when i am on route and logout page isn't refresh and I am stay on route for logged users.
import React, { Component } from "react";
import AuthHelper from "./AuthHelper";
export default function withAuth(AuthComponent) {
const Auth = new AuthHelper();
class AuthWrapped extends Component {
constructor(props) {
super(props);
this.state = {
confirm: null,
loaded: false
};
}
checkLogged = () => {
if (!Auth.loggedIn()) {
const parent = this.props.parent;
const obj = this.props.routes
.filter(v => v.id === parent);
this.props.history.replace(obj[0].path);
} else {
try {
const confirm = Auth.getConfirm();
this.setState({
confirm: confirm,
loaded: true
});
} catch (err) {
Auth.logout();
this.props.history.replace("/");
}
}
}
componentDidMount() {
this.checkLogged();
}
render() {
if (this.state.loaded) {
if (this.state.confirm) {
return (
<AuthComponent
history={this.props.history}
confirm={this.state.confirm}
/>
);
} else {
return null;
}
} else {
return null;
}
}
};
return AuthWrapped;
}
I believe that you are using the authentication system the wrong way
In React everything should exist in a hierarchical manner.
In your case, you have an Auth state that would change and when the loggedIn state changes, everything should re-render. the correct way to do this is using the Context API to handle the logged in state so when the state changes, the whole screen would re-render
here is the solution to your problem:
AuthContext.js
const AuthContext = React.createContext();
export class AuthProvider extends React.Component {
state = {
isLoggedIn: false,
};
login = (username, password) => {
someLoginRequestToServer(username, password).then(response => {
this.setState({
isLoggedIn: response.isLoggedIn,
});
});
};
logout = () => {
someLogoutRequestToServer().then(() => {
this.setState({ isLoggedIn: false });
});
};
render() {
return (
<AuthContext.Provider
value={{
loggedIn: this.state.isLoggedIn,
login: this.login,
logout: this.logout,
}}>
{this.props.children}
</AuthContext.Provider>
);
}
}
export const AuthConsumer = AuthContext.Consumer;
SomeCustomAuthComponent
class CustomAuthComponent extends React.Component {
render() {
return (
<AuthConsumer>
{({ loggedIn, login, logout }) => (
<div>
<p>You Are {loggedIn ? 'Logged in' : 'Logged out'}</p>
<button onClick={loggedIn ? () => logout() : () => login('abcd', '12345')} />
</div>
)}
</AuthConsumer>
);
}
}
Or you can use the redux for state management and react-redux as it uses the react Context API under the hood.
hope this helps you! :)
the problem lays here
componentDidMount() {
this.checkLogged();
}
you're checking if the user is logged only when the component is mounted (after the instantiation). you should be checking it every time the page updates, you have to identify a way to handle this mechanism for example by using the componentDidUpdate hook.
export default function withAuth(AuthComponent) {
const Auth = new AuthHelper();
class AuthWrapped extends Component {
constructor(props) {
super(props);
this.state = {
confirm: null,
loaded: false
};
}
checkIsNotLogged = () => {
const parent = this.props.parent;
const obj = this.props.routes
.filter(v => v.id === parent);
this.props.history.replace(obj[0].path);
}
checkLogged = () => {
if (!Auth.loggedIn()) {
this.checkIsNotLogged();
} else {
try {
const confirm = Auth.getConfirm();
this.setState({
confirm: confirm,
loaded: true
});
} catch (err) {
Auth.logout();
this.props.history.replace("/");
}
}
}
componentDidMount() {
this.checkLogged();
}
componentDidUpdate() {
// do not call here the checkLogged method otherwise you could trigger an infinite loop
this.checkIsNotLogged();
}
render() {
if (this.state.loaded) {
if (this.state.confirm) {
return (
<AuthComponent
history={this.props.history}
confirm={this.state.confirm}
/>
);
} else {
return null;
}
} else {
return null;
}
}
};
return AuthWrapped;
}
I have a React app with a bunch of components with a few similarities:
Most components load data from Firebase at construction
Most components have an input form that the user can interact with
Most components have a simple view
My issue is that the state becomes hard to manage fairly early on as I try to keep all state in the top level component. For instance, I have the component below that let's the user create a new product, add a few images and place a custom marker on one of the images.
My current setup for all components is that there is a currentEntry which represents the entry that the user is currently editing which I initialize with a blank state.
Is it best practice to keep all state in the top component like this or should I rethink my structure?
import React, { Component } from 'react';
import CreateEntryForm from "../../components/entries/createEntryForm";
import { withStyles } from 'material-ui/styles';
import ViewImageDialog from "../../components/entries/viewImageDialog";
import {FirebaseList} from "../../utils/firebase/firebaseList";
import {generateFilename, removeItem, snapshotToArray} from "../../utils/utils";
import {
Redirect
} from 'react-router-dom';
import AppBar from "../../components/appBar";
import Spinner from "../../components/shared/spinner";
import firebase from 'firebase';
const styles = theme => ({
root: {
margin: theme.spacing.unit*2,
}
});
const initialFormState = {
currentProduct: null,
selectedProducts: [],
selectedUploads: [],
selectedMarkedImage: null,
productQuantity: '',
locationDescription: '',
comments: '',
currentUpload: null,
username: 'username'
};
const initialFormErrorState = {
selectProductError: '',
};
class CreateEntry extends Component {
constructor() {
super();
this.state = {
products: [],
job: null,
currentEntry: {...initialFormState},
formErrors: initialFormErrorState,
uploadLoading: false,
markedImageLoaded: false,
attachmentDialogOpen: false,
openAttachment: null,
markerPosition: null,
availableAttachments: [],
entries: [],
redirect: false,
loading: true,
isEditing: false
};
this.firebase = new FirebaseList('entries');
this.handleSubmit = this.handleSubmit.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.setMarker = this.setMarker.bind(this);
this.handleAttachmentDialogOpen = this.handleAttachmentDialogOpen.bind(this);
this.saveMarkedImage = this.saveMarkedImage.bind(this);
this.handleMarkedImageLoaded = this.handleMarkedImageLoaded.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleProgress = this.handleProgress.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
this.handleUploadSuccess = this.handleUploadSuccess.bind(this);
}
componentDidMount() {
this.firebase.path = `entries/${this.props.match.params.id}`;
this.jobId = this.props.match.params.id;
this.entryId = this.props.match.params.entry || null;
this.firebase.db().ref(`jobs/${this.props.match.params.id}`).on('value', (snap) => {
const job = {
id: snap.key,
...snap.val()
};
this.setState({
job: job,
loading: false,
})
});
this.firebase.databaseSnapshot(`attachments/${this.jobId}`).then((snap) => {
const attachments = snapshotToArray(snap);
this.setState({availableAttachments: attachments})
});
this.firebase.databaseSnapshot(`entries/${this.jobId}`).then((snap) => {
const entries = snapshotToArray(snap);
const otherMarkedEntries = entries.filter(entry => entry.id !== this.entryId);
this.setState({otherMarkedEntries: otherMarkedEntries})
});
if (this.entryId) {
this.firebase.databaseSnapshot(`entries/${this.jobId}/${this.entryId}`).then((entry) => {
const updatedEntry = Object.assign({...initialFormState}, entry.val());
this.setState({
currentEntry: updatedEntry,
isEditing: !!this.entryId
})
});
}
}
validate() {
const errors = {...initialFormErrorState};
let isError = false;
if(this.state.currentEntry.selectedProducts.length === 0) {
errors.selectProductError = "You must select at least one product";
isError = true;
}
this.setState({formErrors: errors});
return isError
}
handleSubmit() {
const err = this.validate();
if(!err) {
if(this.state.job && this.state.currentEntry) {
if(!this.state.isEditing) {
const newEntry = {
...this.state.currentEntry,
'creationDate': Date.now()
};
let newEntryRef = this.firebase.db().ref(`entries/${this.jobId}`).push();
newEntryRef.set(newEntry);
if (this.state.currentEntry.selectedMarkedImage !== null) {
this.firebase.db().ref(`attachments/${this.jobId}/${newEntry.currentUpload.id}/markings/${newEntryRef.key}`)
.set(this.state.currentEntry.selectedMarkedImage)
}
this.setState({redirect: 'create'});
} else {
const updatedEntry = {
...this.state.currentEntry
};
const newLogEntry = {
'lastUpdated': Date.now(),
'updatedBy': 'username'
};
this.firebase.db().ref(`log/${this.jobId}/${this.entryId}`).push(newLogEntry);
this.firebase.update(this.entryId, updatedEntry)
.then(() => this.setState({redirect: 'edit'}));
}
}
}
};
handleInputChange = name => e => {
e.preventDefault();
const target = e.target;
const value = target.value;
if (name === 'currentUpload') {
this.handleAttachmentDialogOpen(this.state.job.selectedUploads);
}
this.setState({ currentEntry: { ...this.state.currentEntry, [name]: value } });
};
addSelectedChip = () => {
if (this.state.currentEntry.currentProduct) {
const updatedCurrentProduct = {
...this.state.currentEntry.currentProduct,
'productQuantity': this.state.currentEntry.productQuantity
};
const updatedSelectedProducts = [...this.state.currentEntry.selectedProducts, updatedCurrentProduct];
const updatedEntryStatus = {
...this.state.currentEntry,
selectedProducts: updatedSelectedProducts,
currentProduct: null,
productQuantity: ''
};
this.setState({currentEntry: updatedEntryStatus});
}
};
handleRequestDeleteChip = (data, group) => {
const itemToChange = new Map([['product', 'selectedProducts'], ['upload', 'selectedUploads']]);
const selected = itemToChange.get(group);
const updatedSelectedItems = removeItem(this.state.currentEntry[selected], data.id);
const updatedEntryStatus = {
...this.state.currentEntry,
[selected]: updatedSelectedItems
};
this.setState({currentEntry: updatedEntryStatus});
};
handleAttachmentDialogOpen = (attachment) => {
this.setState({
attachmentDialogOpen: true,
openAttachment: attachment
});
};
handleAttachmentDialogClose =() => {
this.setState({attachmentDialogOpen: false})
};
saveMarkedImage() {
const markedImage = {
'attachment': this.state.openAttachment[0],
'position': this.state.markerPosition
};
const updatedCurrentEntry = {
...this.state.currentEntry,
'selectedMarkedImage': markedImage
};
this.setState({
currentEntry: updatedCurrentEntry
});
this.handleAttachmentDialogClose()
}
setMarker(e) {
const dim = e.target.getBoundingClientRect();
const position = {
'pageX': e.pageX - dim.left -25,
'pageY': e.pageY - dim.top - 50
};
this.setState({markerPosition: position});
}
handleMarkedImageLoaded() {
this.setState({markedImageLoaded: true})
}
filterProducts(selected, available) {
if(this.state.job) {
const selectedProductNames = [];
selected.forEach(product => selectedProductNames.push(product.name));
return available.filter(product => !selectedProductNames.includes(product.name))
}
}
handleUploadStart = () => this.setState({uploadLoading: true, progress: 0});
handleProgress = (progress) => this.setState({progress});
handleUploadError = (error) => {
this.setState({uploadLoading: false});
console.error(error);
};
handleUploadSuccess = (filename) => {
firebase.storage().ref('images').child(filename).getDownloadURL().then(url => {
const getNameString = (f) => f.substring(0,f.lastIndexOf("_"))+f.substring(f.lastIndexOf("."));
const uploadItem = {"name": getNameString(filename), "url": url, "id": this.generateRandom()};
const updatedSelectedUploads = [...this.state.currentEntry.selectedUploads, uploadItem];
const updatedEntryStatus = {
...this.state.currentEntry,
selectedUploads: updatedSelectedUploads
};
this.setState({
uploadLoading: false,
currentEntry: updatedEntryStatus
});
});
};
generateRandom() {
return parseInt(Math.random());
}
render() {
const {classes} = this.props;
const filteredProducts = this.filterProducts(this.state.currentEntry.selectedProducts, this.state.job && this.state.job.selectedProducts);
const title = this.state.isEditing ? "Edit entry for" : "Add entry for";
const redirectRoute = this.state.redirect
? `/entries/${this.props.match.params.id}/${this.state.redirect}`
: `/entries/${this.props.match.params.id}`;
return (
<section>
<AppBar title={`${title} ${this.state.job && this.state.job.jobId}`} route={`/entries/${this.props.match.params.id}`}/>
{this.state.loading
? <Spinner />
: <div className={classes.root}>
<ViewImageDialog open={this.state.attachmentDialogOpen}
handleRequestClose={this.handleAttachmentDialogClose}
attachment={this.state.currentEntry.currentUpload}
setMarker={this.setMarker}
markerPosition={this.state.markerPosition || this.state.selectedMarkedImage && this.state.selectedMarkedImage.position}
saveMarkedImage={this.saveMarkedImage}
markedImageLoaded={this.state.markedImageLoaded}
handleMarkedImageLoaded={this.handleMarkedImageLoaded}
otherMarkedEntries={this.state.otherMarkedEntries}
/>
<CreateEntryForm handleInputChange={this.handleInputChange}
handleSubmit={this.handleSubmit}
availableProducts={filteredProducts}
addSelectedChip={this.addSelectedChip}
handleRequestDeleteChip={this.handleRequestDeleteChip}
job={this.state.job}
availableAttachments={this.state.availableAttachments}
uploadLoading={this.state.uploadLoading}
handleAttachmentDialogOpen={this.handleAttachmentDialogOpen}
markedImageLoaded={this.state.markedImageLoaded}
handleMarkedImageLoaded={this.handleMarkedImageLoaded}
isEditing={this.state.isEditing}
handleProgress={this.handleProgress}
handleUploadError={this.handleUploadError}
handleUploadSuccess={this.handleUploadSuccess}
firebaseStorage={firebase.storage().ref('images')}
filename={file => generateFilename(file)}
otherMarkedEntries={this.state.otherMarkedEntries}
{...this.state.currentEntry}
{...this.state.formErrors}
/>
{this.state.redirect && <Redirect to={redirectRoute} push />}
</div>}
</section>
);
}
}
export default withStyles(styles)(CreateEntry);
A centralised global state is a good pattern for state that needs to be global to the whole application. For me, https://redux.js.org/ is the best state engine for react applications.
When I build react/redux applications, I tend to start storing state at the lowest component level I can, and then move it up the component tree and finally into global redux state as and when it is required.
For example, a piece of state that stores whether a div is being hovered over could be stored at component level because it doesn't affect other components, but a piece of state that stores whether a modal is open might need to be in global redux state, because other parts of the application would need to know this.
I would really recommend trying out redux, or at least reading the docs.