I am trying to do something as soon as a dispatch action finishes, and a similar question led me to try to use .then. I get an error that it doesn't exist, this.props.dispatch().then is not a function:
export function unpackStore(redux_store, namespace) {
// namespace is determined by what name you gave each reducer in combineReducers; client/reducers/index.js
let final_props = {};
let KEYS = Object.keys(redux_store[namespace]);
for (let key of KEYS) {
final_props[key] = redux_store[namespace][key];
}
return final_props;
}
export function basicUnpackStoreClosure(namespace) {
return function(store) {
let props = unpackStore(store, namespace);
return props;
}
}
#connect(basicUnpackStoreClosure('login_info'))
export default class LoginPage extends MyComponent {
constructor(props) {
let custom_methods = [
'handleLoginOrRegisterToggle',
'handleOnKeyDownInInputs',
'onLoginSubmit',
'onRegisterSubmit',
]
super(props, custom_methods);
this.state = {
mode: 'login',
email: '',
password: '',
password_confirm: ''
};
if (props.mode == 'register') {
this.state.mode = 'register';
}
}
onLoginSubmit() {
let self = this;
let reroute = function() {
browserHistory.push(self.props.destination_url);
}
this.props.dispatch({type: 'LOG_IN', payload: "fake#fake.com"})
.then((response) => {
browserHistory.push(self.props.destination_url);
})
}
We also tried the way you do something when this.setState finishes, passing it as a second arg:
this.props.dispatch({type: 'LOG_IN', payload: "fake#fake.com"}, reroute)
neither worked. The this.props.dispatch seems to come for free with using the #connect decorator. How can I run something only after this Redux store is updated with the "LOG_IN" action?
Normally the connect HOC is used to bind the actions with a component. For an example,
export default connect(mapStateToProps, mapDispatchToProps)(ApplicationContainer);
After you do this, then this.props.getApplications will be bound where getApplications is the action you need to fire.
As per your comment, if you want to have access to this.props.dispatch INSTEAD of binding actions, simply call connect() without passing any mappers, and the default behavior will inject dispatch.
You may install 'redux-promise' middleware and include it in your store. The problem here is that the promise is not getting fulfilled. Including 'redux-promise' middleware will solve the problem
Related
I have some context. I store there user roles.
const RolesContext = createContext({roles: []});
function RolesContextProvider({children}) {
const [roles, setRoles] = useState([]);
async function check(newRoles) {
const missing = compareArrayWithArray(newRoles, roles);
if (missing.length !== 0) {
await User.roles(newRoles).then(((res) => {
const updatedRoles = roles.concat(res.data);
setRoles(updatedRoles);
}));
}
}
const defaultContext = {
roles,
check,
};
return (
<RolesContext.Provider value={defaultContext}>
{children}
</RolesContext.Provider>
);
}
export {RolesContext, RolesContextProvider};
When initiating component I run check roles
export default function Users() {
const UsersComposition = compose(
connect(mapStateToProps, mapDispatchToProps)
)(ConnectedUsers);
const context = useContext(RolesContext);
const {roles, check} = context;
useEffect(() => {
check(['roles', 'to', 'check']);
}, [check]);
return <UsersComposition roles={roles}/>;
};
What it does...App is crashing due to inifite update state loop. It makes dozens of same requests with same payload. How it should be done? Thanks for suggestions.
In order to fix the infinite loop you'll need to preserve check function's identity between renders. One way to do this is to save it with useRef (you'll need to pass existing roles as the second parameter):
const check = useRef(async (newRoles, roles) => {
...
});
const defaultContext = {
roles,
check: check.current,
};
You may also consider implementing Users as a class component and call check in componentDidMount:
class Users extends React.Component {
static contextType = RolesContext;
componentDidMount() {
this.context.check(['roles', 'to', 'check']);
}
render() {
...
}
}
It seems like you're calling check in a loop. See if I'm right.
RolesContextProvider provides the context roles and check
Users access the context and calls check
Check updates roles in RolesContextProvider with setRoles
setRoles triggers update in RolesContextProvider which changes the context
And the context change updates Users
And the cycle repeats
I am implementing a Welcome Display web app that takes a guest name received from RabbitMQ and populates it on the screen. In the callback function of the stompClient.subscribe(... I want to call the function to change the state of the reservation and view on the screen. When I call the function it says the function is not defined. How can I change the state every time I receive the message?
import React from 'react';
import '../css/App.css'
import WelcomeVisitor from '../pages/WelcomeVisitor';
import ThankYou from '../pages/ThankYou';
import Stomp from 'stompjs'
class App extends React.Component {
constructor(props){
super(props)
this.state = {
currentView: 'ThankYou',
currentReservation: null
}
this.siteId = props.match.params.siteId
this.bayNumber = props.match.params.bayNumber
this.changeView = this.changeView.bind(this)
this.connectRabbit = this.connectRabbit.bind(this)
}
changeView(view){
this.setState({
currentView: view
})
}
changeReservation(reservation){
this.setState({
currentReservation: reservation
})
}
render(){
let view = ''
this.connectRabbit(this.siteId, this.bayNumber)
if(this.state.currentView === 'ThankYou'){
view = <ThankYou changeView={this.changeView}/>
} else if(this.state.currentView === 'WelcomeVisitor') {
view = <WelcomeVisitor guestName='Quinton Thompson'/>
}
return (
<div className="App">
{view}
</div>
)
}
connectRabbit(siteId, bayNumber){
let stompClient
var ws = new WebSocket('ws://localhost:15674/ws')
const connectHeaders = {
'login': 'guest',
'passcode': 'guest',
}
const queueHeaders = {
'x-queue-name': `${bayNumber}.visit.out.display`,
'durable': 'true',
'auto-delete': 'false'
}
stompClient = Stomp.over(ws)
stompClient.connect(connectHeaders , function(frame){
console.log('Connected')
stompClient.subscribe('/exchange/ds.game/visit.out',function(message){
//changeReservation and changeView is not defined
this.changeReservation(message.body)
this.changeView('WelcomeVisitor')
}, queueHeaders)
console.log('here')
})
}
}
export default App;
The this object in your function callback is likely not referencing the this object in your class.
Changing the function syntax to: (message) => {} and (frame) => {} should make it work. See below:
stompClient.connect(connectHeaders ,(frame) => {
console.log('Connected')
stompClient.subscribe('/exchange/ds.game/visit.out', (message) => {
//changeReservation and changeView is not defined
this.changeReservation(message.body)
this.changeView('WelcomeVisitor')
}, queueHeaders)
console.log('here')
})
While the code snippet above would make your code work,
ideally we should avoid writing these types of callback initializations on the fly ( in render method ), maybe better way of doing it would be creating function calls and referencing those as callbacks. Something like this ( more improvements can be made but just as an example ) :
connectCallback(stompClient, queueHeaders, frame) {
console.log('Connected');
stompClient.subscribe('/exchange/ds.game/visit.out', (message) => {
this.subscribeCallback(message)
}, queueHeaders);
}
subscribeCallback(message) {
this.changeReservation(message.body)
this.changeView('WelcomeVisitor')
}
Then just use the two functions above as a callback in your render code.
Lastly, you might need to bind changeReservation(reservation) also before anything else.
First, I made a small application on the React.js. Using the fetch method, I take the API
And these are the main files of my application:
Index.js:(action)
export const SHOW_AIRPLANES = "SHOW_AIRPLANES";
export function showAirplanes() {
return (dispatch, getState) => {
fetch("https://api.iev.aero/api/flights/25-08-2019").then(response => {
dispatch({ type: SHOW_AIRPLANES, payload: response.data });
});
};
}
airplanes.js:(reducer)
import { SHOW_AIRPLANES } from '../actions'
const initialState = {
list: []
}
export function showAirplanes(state = initialState, action) {
switch (action.type) {
case SHOW_AIRPLANES:
return Object.assign({}, state, {list: action.payload})
default:
return state
}
}
index.js(reducer):
import { combineReducers } from "redux";
import { showAirplanes } from "./airplanes";
const rootReducer = combineReducers({
user: showAirplanes
});
export default rootReducer;
First, you should use the createStore function like so:
const initialData = {}; // whatever you want as initial data
const store = createStore(reducers, initialData, applyMiddleware(thunk));
Then pass it to your provider
<Provider store={store}>
{...}
</Provider
next, when you map your reducers inside the combineReducers function, each key in this object represents a piece of your state. So when you do user: showAirplanes it means that you intend to use it in the mapStateToProps with state.user.list so I think you meant to call it airplane: showAirplanes.
Then, your reducer name is not informative enough, I would suggest to change it to airplanesReducer.
Next issue, the call to fetch returns a response that has JSON that must be resolved.
Change this:
fetch("https://api.iev.aero/api/flights/25-08-2019").then(response => {
dispatch({ type: SHOW_AIRPLANES, payload: response.data });
});
To this:
fetch("https://api.iev.aero/api/flights/25-08-2019")
.then(res => res.json())
.then(response => {
dispatch({ type: SHOW_AIRPLANES, payload: response.body.departure });
});
Note that I've changed the value that you need to resolve from the response as well.
Inside your App.js component you need to create a constructor and bind the renderAirplaneList function to this
// Inside the App class
constructor(props) {
super(props);
this.renderAirplaneList = this.renderAirplaneList.bind(this);
}
And finally (I hope I didn't miss anything else), you map your state in the App.js component to { airplanes: state.airplanes.list} so the name of the prop you expect inside your component is props.airplanes.
renderAirplaneList() {
if (!this.props.airplanes.length) {
return null;
}
const arr = this.props.airplanes || [];
return arr.map(airplane => {
return (
<tr key={airplane.id}>
<td>{airplane.ID}</td>
<td>{airplane.term}</td>
<td>{airplane.actual}</td>
<td>{airplane["airportToID.city_en"]}</td>
</tr>
);
});
}
Make sure you go over the documentation of React and Redux, they have all the information you need.
Good luck.
aren't you suppose to send some parameters to this call?
this.props.showAirplanes()
it seems that it has 2 parameters: state and action, although state seems to have already it's default value
I have two functions one that fetches data from an api and updates state according to that data, and a function that itterates over the data in the state and updates the state with the new data.
My problem is that i cant update the state in the second function. And i dont know where i have to call this function in order for it to be called after the first function and to use the data thats in the state.
export default class Cases extends Component {
constructor(props) {
super(props);
this.state = {
cases: [],
photos: [],
};
this.addPhotos = this.addPhotos.bind(this);
this.getCases = this.getCases.bind(this);
this.renderCases = this.renderCases.bind(this);
}
getCases() {
axios
.get('/cases/api')
.then(response => {
this.setState({
cases: response.data.cases,
photos: response.data.photos,
});
console.log(this.state.cases);
})
.catch((error) => {
if (error) {
console.log(error);
}
});
}
addPhotos() {
const newCases = this.state.cases.map(({ photo_id, ...rest }) => {
const obj = { ...rest };
this.state.photos.find(data => {
if (data.id === photo_id) {
obj.file = data.file;
return true;
}
});
return obj;
});
console.log(this.state.cases);
this.setState({
'cases' : newCases
});
}
renderCases() {
this.addPhotos();
}
componentWillMount() {
this.getCases();
}
render() {
return (
<div>
{this.renderCases()}
</div>
)
}
}
This is what i now have
Where should i call the addPhotos function so it can update the state and still use the existing state data from the getCases function?
Thanks in advance!
So, first thing's first. The lifecycle method componentWillMount() is soon to be deprecated and is considered unsafe to use. You should be using componentDidMount().
As far as using the updated state in your addPhotos function, you can pass setState a callback function. A seemingly simple solution would be to just pass the addPhotos function as a callback into the setState being called in your getCases function.
getCases() {
axios
.get('/cases/api')
.then(response => {
this.setState({
cases: response.data.cases,
photos: response.data.photos,
}, this.addPhotos);
console.log(this.state.cases);
})
.catch((error) => {
if (error) {
console.log(error);
}
});
}
Another solution would be to call addPhotos() from componentDidUpdate instead.
Hope this helps!
Edit: Just some additional background information from the React docs.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
Added some refactoring to your code, should work ok now, read comments for details
export default class Cases extends Component {
constructor(props) {
super(props);
this.state = {
cases: [],
photos: [],
};
this.addPhotos = this.addPhotos.bind(this);
this.getCases = this.getCases.bind(this);
this.renderCases = this.renderCases.bind(this);
}
getCases() {
axios
.get('/cases/api')
.then(this.addPhotos) // don't need to set state, pass data to consumer function
.catch(console.error); // catch always gives error, don't need to check with if statement
}
addPhotos(response) {
const cases = response.data.cases // extract values
const photos = response.data.photos // extract values
// your .map iterator has O^2 complexity (run each item of cases on each item of photos)
// this new .map iterator has O complexity (run each item of cases)
const newCases = cases.map(({ photo_id, ...rest }) => {
const obj = {...rest};
const data = photos.find(item => item.id === photo_id);
if (data) {
obj.file = data.file
}
return obj;
});
this.setState({
cases: newCases,
photos
});
}
componentDidMount() { // better use didMount
this.getCases();
}
render() {
return (<div />)
}
}
This might be a question of best practices but I'd appreciate an explanation on why this doesn't work. I'm using Typescript + Redux + Thunk and trying to call actions like this:
export const requestUserDashboards = createAction<DashboardModel>(Type.REQUEST_USER_DASHBOARDS);
Dispatch in the fetch:
export const fetchDashboards = () => {
return async (dispatch: Dispatch, getState: any) => {
try {
dispatch(requestUserDashboards({
currentDashboard: getState.currentDashboard,
dashboards: getState.dashboards,
hasDashboards: false,
error: getState.error
}))
...
}
})
}
Here's the corresponding reducer:
export const dashboardReducer = handleActions<RootState.DashboardState, DashboardModel>(
{
[DashboardActions.Type.REQUEST_USER_DASHBOARDS]: (state = initialState, action): RootState.DashboardState => ({
currentDashboard: action.payload!.currentDashboard,
dashboards: action.payload!.dashboards,
hasDashboards: action.payload!.hasDashboards,
error: action.payload!.error
})
},
initialState
);
dispatch is working, however, getState doesn't correctly collect the current store state. I'm testing this by doing the following in the component receiving the updated store:
componentWillReceiveProps(nextProps: Login.Props) {
console.log(nextProps.defaultAccounts.defaultAccount);
}
Calling this in the component using:
this.props.defaultAccountActions.fetchUserDefaultAccount();
The action is working as the values from the fetch are being captured.
However, where I am using the getState.xxxx, these values are returning as undefined:
index.tsx:84 Uncaught TypeError: Cannot read property 'defaultAccount' of undefined
The initialState from my reducer is working. I can see this from doing the console.log(this.props.defaultAccounts.defaultAccount) from the componentWillMount() function.
I'm not sure what else I can provide. I think I'm actually just fundamentally misunderstanding how actions/reducers manage the store.
Questions
I am trying to get the current store values by using the getState.xxxx in the dispatch. Is this the correct way to do this?
isn't getState a function in that place? So you would need to do something
const state = getState();
and then use state inside dispatch
found in documentation, yeah it is a function at that place so you should firstly invoke a function to get state and then use it (e.g. from documentation below)
function incrementIfOdd() {
return (dispatch, getState) => {
const { counter } = getState();
if (counter % 2 === 0) {
return;
}
dispatch(increment());
};
}
If you are using mapstatetoprops in your component you can use that to get the values from store. mapStateToProps first argument is actually the Redux state. It is practically an abstracted getState().
const mapStateToProps = function(state, ownProps) {
// state is equivalent to store.getState()
// you then get the slice of data you need from the redux store
// and pass it as props to your component
return {
someData: state.someData
}
}