I have my store using the Context API to create a user, where there are two screens for completing the registration.
The first screen has several input fields that I control with React Hook Form, and when I enter all the data, I navigate to the other screen and fill in 2 more inputs, one being a checkbox with several options.
On the first screen, I can get all the information from the user, and on the second screen, I can get only the values of the checked boxes, and the location input that I have, it cannot insert into the state of my Context API.
src/store/CreatePersonalAccount/index.tsx
import { createContext, useReducer } from 'react'
import { reducer } from './reducer'
export const initialState: PersonalAccount = {
avatar: '',
name: '',
email: '',
password: '',
location: '',
occupationCategory: '',
occupationSubcategories: []
}
export const CreatePersonalAccountContext = createContext<
IContext<PersonalAccount>
>({
state: initialState,
dispatch: () => {}
})
export const CreatePersonalAccountContextProvider: React.FC = ({
children
}) => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<CreatePersonalAccountContext.Provider value={{ state, dispatch }}>
{children}
</CreatePersonalAccountContext.Provider>
)
}
CreatePersonalAccount/action.ts
export enum CreatePersonalAccountActions {
SET_CREATE_PERSONAL_ACCOUNT = 'SET_CREATE_PERSONAL_ACCOUNT',
}
export const setPersonalAccount = (payload: Partial<PersonalAccount>) => {
return {
type: CreatePersonalAccountActions.SET_CREATE_PERSONAL_ACCOUNT,
payload
}
}
CreatePersonalAccount/reducer.ts
import { initialState } from './index'
import { CreatePersonalAccountActions as ActionTypes } from './action'
export const reducer = (state: PersonalAccount, action: IAction) => {
switch (action.type) {
case ActionTypes.SET_CREATE_PERSONAL_ACCOUNT:
return {
...state,
...action.payload
}
case ActionTypes.RESET:
return {
...initialState
}
default:
return state
}
}
In my user creation routes, I have hooks that can be used both on the first screen and on the second screen.
import { useContext } from 'react'
import { CreatePersonalAccountContext } from 'store/CreatePersonalAccount'
import {
setPersonalAccount,
resetPersonalAccount
} from 'store/CreatePersonalAccount/action'
export function usePersonalAccount() {
const { state, dispatch } = useContext(CreatePersonalAccountContext)
const setPersonalInformation = (state: PersonalAccountInformation) =>
dispatch(setPersonalAccount(state))
const setPersonalLocation = (location: string) =>
dispatch(setPersonalAccount({ location }))
const setPersonalOccupation = (
params: Partial<
Pick<PersonalAccount, 'occupationCategory' | 'occupationSubcategories'>
>
) => dispatch(setPersonalAccount(params))
const clearPersonalAccount = () => dispatch(resetPersonalAccount())
const submit = () => {
console.log('Enviar pra API', state)
}
return {
state,
submit,
setPersonalLocation,
clearPersonalAccount,
setPersonalOccupation,
setPersonalInformation
}
}
The setPersonalInformation function is used on the first screen with the user information, and the other functions setPersonalLocation and setPersonalOccupation are used on the second screen of the user information, the problem is the setPersonalLocation function that cannot update the state with the new location property.
This function below is the function I use in the first info screen, and it works correctly, updating the avatar, name, document, email, password fields.
SignUp/PersonalAccount/PersonalInformation/index.tsx
...
function handleRegisterPersonal(data: PersonalAccountInformation) {
const personalInformationData = {
...data,
avatar: image
}
setPersonalInformation(personalInformationData)
navigation.navigate('PersonalLocationAndOccupation')
}
...
And on the screen where I would like to update the location property is the function below.
SignUp/PersonalAccount/PersonalLocationAndOccupation
function registerPersonalAccountForm({ location }: PersonalAccountLocation) {
setPersonalLocation(location)
navigation.navigate('Welcome')
}
And my final state is this, but without the location property assigned
Object {
"avatar": "file:///Users/vagnerwentz/Library/Developer/CoreSimulator/Devices/BCB82E0B-A81E-495A-9917-80C315CEE735/data/Containers/Data/Application/FB8A6B2E-3F8F-4EBE-9AC0-128BBC80A84E/Library/Caches/ExponentExperienceData/%2540anonymous%252FPlugApp0800-fe3f8358-2821-44a3-a206-d0e1909b03e0/ImagePicker/FF615EB9-E385-4397-9198-E49FE6CD8CE4.jpg",
"documentation": "1234567890",
"email": "email#email.com",
"location": "",
"name": "John Doe",
"occupationCategory": "Developer",
"occupationSubcategories": Array [
Object {
"id": "frontend",
"title": "Front-end",
},
Object {
"id": "backend",
"title": "Back-end",
},
],
"password": "1234567890",
}
I've already tried to put it in the property inside the useForm, assign the context field, with my Context, but without success, I also tried to put a setTimeout to simulate an asynchronous call, and I didn't succeed either.
Related
Created a new service .js called room.module.js
and inside my Vue view I have the following event on form submit:
submit: function(e) {
e.preventDefault();
var name = this.$refs.name.value;
var capacity = this.$refs.places.value;
// dummy delay
setTimeout(() => {
// send update request
this.$store.dispatch(CREATE_ROOM, {
"name": name,
"places": capacity
});
}, 2000);
and my service room.module.js:
// action types
import ApiService from "#/core/services/api.service";
import JwtService from "#/core/services/jwt.service";
export const CREATE_ROOM = "createNewRoom";
// mutation types
export const SET_ROOM_INFO = "setRoomInfo";
const state = {
room_info: {
name: "Room 1",
places: 10,
status: 1
}
};
const getters = {
currentRoomInfo(state) {
return state.room_info;
}
};
const actions = {
[CREATE_ROOM](context, payload) {
if (JwtService.getToken()) {
ApiService.setHeader();
ApiService.put("/room/create", payload).then(({ data }) => {
context.commit(SET_ROOM_INFO, payload);
return data;
});
}
}
};
const mutations = {
[SET_ROOM_INFO](state, room_info) {
state.room_info = room_info;
}
};
export default {
state,
actions,
mutations,
getters
};
but when I submit the form, the following error occurs:
[vuex] unknown action type: createNewRoom
I know I'm missing something, but can't figure out what.
Any ideas? Thank you!
It seems it needs to be added to the Vuex store.
I am fairly new to React Native and Redux. I am fetching data on Restaurants from an API route using axios,and my goal is to display the Restaurants, but when I console.log in the components I get an empty object {}.
Here is my data sample:
[{
"id_supplier":1,
"supplier_name":"Grill Park",
"supplier_description":"Order food at Grill-Park. Masters of Italian Pizza.",
"supplier_image":"restaurant1.jpg",
"createdAt":"2020-08-28T21:00:00.000Z",
"updatedAt":"0000-00-00"
},
{
"id_supplier":2,
"supplier_name":"Bobbies burger",
"supplier_description":"Order a burger on the go. Beef, Cheese, Hamburger, Vegan Burgers",
"supplier_image":"restaurant2.jpg",
"createdAt":"2020-08-28T21:00:00.000Z",
"updatedAt":"0000-00-00"
},
{
"id_supplier":3,
"supplier_name":"Chicken n licken",
"supplier_description":"Renowned for serving tasty fried chicken pieces and a wide range of other menu items.",
"supplier_image":"restaurant3.jpg",
"createdAt":"2020-08-28T21:00:00.000Z",
"updatedAt":"0000-00-00"
},
{
"id_supplier":4,
"supplier_name":"French House Tarvern",
"supplier_description":"Grab a sandwich at our Pick 'n' Mix Sandwich hip and fun environment",
"supplier_image":"restaurant4.jpg",
"createdAt":"2020-08-28T21:00:00.000Z",
"updatedAt":"0000-00-00"
}]
Here is my model:
export interface RestaurantModel {
id_supplier: number;
supplier_name: string;
supplier_description: string;
supplier_image: string;
createdAt: Date;
updatedAt: Date;
}
export interface RestaurantState{
restaurants_avail: RestaurantModel;
//add others
}
My action file looks as follows
export const getRestaurants = ()=>{
return async(dispatch : Dispatch<RestaurantAction>) =>{
try{
const response = await axios.get<RestaurantModel>(`${BASE_URL}/restaurants`)
console.log("respond now", response.data)//This returns an array of the restaurants
if(response){
dispatch({
type: 'ON_GET_RESTAURANTS',
payload: response.data
})
} else {
dispatch({
type: 'ON_RESTAURANT_ERROR',
payload: 'RESTAURANTS UNAVAILABLE'
})
}
}catch(error){
dispatch({
type: 'ON_RESTAURANT_ERROR',
payload: error
})
}
}
}
When I console log the value of of the response.data an array of the restaurants is returned.
My reducer code looks as follows
const initialState: RestaurantState = {
restaurants_avail: {} as RestaurantModel,
}
const RestaurantReducer = (state: RestaurantState = initialState, action: RestaurantAction) =>{
switch(action.type){
case 'ON_GET_RESTAURANTS':
console.log(action.payload)//this returns the updated state as expected
return{
...state,
restaurants_avail: action.payload
}
default:
return state
}
}
I have all my reducers combined in rootreducer then my store looks as follows.
const store = createStore(rootReducer,composeWithDevTools(
applyMiddleware(thunk))
)
console.log("Value of store state", store.getState())
export {store};
store.getState() returns empty objects.
Here is how I intend to print out the array in my Component.
interface HomeProps {
restaurantReducer: RestaurantState;
getRestaurants: Function;
}
const _HomeScreen: React.FC<HomeProps> = (props) => {
const { restaurants_avail } = props.restaurantReducer;
console.log("restaurants array",restaurants_avail)//This returns an empty object
useEffect(() => {
console.log(props.getRestaurants())//This return an array object with all the restaurants
})
I don't understand where am going wrong as console.log(restaurants_avail) should print out the array, but it instead returns an empty {}
Here is the React Native Debugger state chart.
I have an application that has to return only future events. I am able to parse through my firebase realtime db and return only future events in my action. It then passes this array of data to my reducer. In my reducer, I am logging the payload. The console log displays the data I want. Now in my component screen, I console log to see what data I should see from the reducer and it is empty.
My action:
export const getAllEvents = () => {
var currentTime = Date.now();
return (dispatch) => {
var ref = firebase.database().ref('Events/');
ref.orderByChild("availableSpots").on("value", function(snapshot) {
snapshot.forEach(child => {
var time = child.val().epochTime;
if (currentTime < time) {
console.log("Events redux: ", child.val());
dispatch({ type: GET_EVENT_LIST, payload: child.val() });
}
})
});
});
}
}
As you can see I am console logging the child.val() which comes out as expected below:
Now I have this hooked up to my reducer via dispatch.
import { GET_EVENT_LIST } from '../actions/types';
const INITIAL_STATE = {
eventList: [],
};
const eventListReducer = (state = INITIAL_STATE, action) => {
switch (action.type){
case GET_EVENT_LIST:
console.log("action count", action.payload);
return { ...state, eventList: [...state.eventList, action.payload] };
default:
console.log("default ");
return state;
}
};
export default eventListReducer;
As you can see the console log of the action.payload is being logged and produces an expected result:
Now back in my component screen:
import { getUserThunk, getAllEvents } from '../actions';
import {connect} from 'react-redux';
class Home extends Component {
componentWillMount() {
console.log("Component will mount");
this.props.getUserThunk();
this.props.getAllEvents();
}
render() {
console.log("usersadf ", this.props.userReducer)
console.log("Props in home ", this.props.eventListReducer)
return (
//some return code here
);
}
export default connect(
state=>({userReducer: state.userReducer, eventListReducer: state.eventListReducer}),
{ getUserThunk, getAllEvents }
)(Home);
Now as you can see I do have different redux action and reducer hooked up and it comes through. However, the one in question returns empty.
There are plenty of tutorials how to test effects in ngrx 3.
However, I've found only 1 or 2 for ngrx4 (where they removed the classical approach via EffectsTestingModule ), e.g. the official tutorial
However, in my case their approach doesn't work.
effects.spec.ts (under src/modules/list/store/list in the link below)
describe('addItem$', () => {
it('should return LoadItemsSuccess action for each item', async() => {
const item = makeItem(Faker.random.word);
actions = hot('--a-', { a: new AddItem({ item })});
const expected = cold('--b', { b: new AddUpdateItemSuccess({ item }) });
// comparing marbles
expect(effects.addItem$).toBeObservable(expected);
});
})
effects.ts (under src/modules/list/store/list in the link below)
...
#Effect() addItem$ = this._actions$
.ofType(ADD_ITEM)
.map<AddItem, {item: Item}>(action => {
return action.payload
})
.mergeMap<{item: Item}, Observable<Item>>(payload => {
return Observable.fromPromise(this._listService.add(payload.item))
})
.map<any, AddUpdateItemSuccess>(item => {
return new AddUpdateItemSuccess({
item,
})
});
...
Error
should return LoadItemsSuccess action for each item
Expected $.length = 0 to equal 1.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: AddUpdateItemSuccess({ payload: Object({ item: Object({ title: Function }) }), type: 'ADD_UPDATE_ITEM_SUCCESS' }), error: undefined, hasValue: true }) }).
at compare (webpack:///node_modules/jasmine-marbles/index.js:82:0 <- karma-test-shim.js:159059:33)
at Object.<anonymous> (webpack:///src/modules/list/store/list/effects.spec.ts:58:31 <- karma-test-shim.js:131230:42)
at step (karma-test-shim.js:131170:23)
NOTE: the effects use a service which involves writing to PouchDB. However, the issue doesn't seem related to that
and also the effects work in the running app.
The full code is a Ionic 3 app and be found here (just clone, npm i and npm run test)
UPDATE:
With ReplaySubject it works, but not with hot/cold marbles
const item = makeItem(Faker.random.word);
actions = new ReplaySubject(1) // = Observable + Observer, 1 = buffer size
actions.next(new AddItem({ item }));
effects.addItem$.subscribe(result => {
expect(result).toEqual(new AddUpdateItemSuccess({ item }));
});
My question was answered by #phillipzada at the Github issue I posted.
For anyone checking this out later, I report here the answer:
Looks like this is a RxJS issue when using promises using marbles. https://stackoverflow.com/a/46313743/4148561
I did manage to do a bit of a hack which should work, however, you will need to put a separate test the service is being called unless you can update the service to return an observable instead of a promise.
Essentially what I did was extract the Observable.fromPromise call into its own "internal function" which we can mock to simulate a call to the service, then it looks from there.
This way you can test the internal function _addItem without using marbles.
Effect
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
import { Injectable } from '#angular/core';
import { Actions, Effect } from '#ngrx/effects';
import { Action } from '#ngrx/store';
import { Observable } from 'rxjs/Observable';
export const ADD_ITEM = 'Add Item';
export const ADD_UPDATE_ITEM_SUCCESS = 'Add Item Success';
export class AddItem implements Action {
type: string = ADD_ITEM;
constructor(public payload: { item: any }) { }
}
export class AddUpdateItemSuccess implements Action {
type: string = ADD_UPDATE_ITEM_SUCCESS;
constructor(public payload: { item: any }) { }
}
export class Item {
}
export class ListingService {
add(item: Item) {
return new Promise((resolve, reject) => { resolve(item); });
}
}
#Injectable()
export class SutEffect {
_addItem(payload: { item: Item }) {
return Observable.fromPromise(this._listService.add(payload.item));
}
#Effect() addItem$ = this._actions$
.ofType<AddItem>(ADD_ITEM)
.map(action => action.payload)
.mergeMap<{ item: Item }, Observable<Item>>(payload => {
return this._addItem(payload).map(item => new AddUpdateItemSuccess({
item,
}));
});
constructor(
private _actions$: Actions,
private _listService: ListingService) {
}
}
Spec
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
import { async, TestBed } from '#angular/core/testing';
import { Actions } from '#ngrx/effects';
import { Store, StoreModule } from '#ngrx/store';
import { getTestActions, TestActions } from 'app/tests/sut.helpers';
import { AddItem, AddUpdateItemSuccess, ListingService, SutEffect } from './sut.effect';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
describe('Effect Tests', () => {
let store: Store<any>;
let storeSpy: jasmine.Spy;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({})
],
providers: [
SutEffect,
{
provide: ListingService,
useValue: jasmine.createSpyObj('ListingService', ['add'])
},
{
provide: Actions,
useFactory: getTestActions
}
]
});
store = TestBed.get(Store);
storeSpy = spyOn(store, 'dispatch').and.callThrough();
storeSpy = spyOn(store, 'select').and.callThrough();
}));
function setup() {
return {
effects: TestBed.get(SutEffect) as SutEffect,
listingService: TestBed.get(ListingService) as jasmine.SpyObj<ListingService>,
actions$: TestBed.get(Actions) as TestActions
};
}
fdescribe('addItem$', () => {
it('should return LoadItemsSuccess action for each item', async () => {
const { effects, listingService, actions$ } = setup();
const action = new AddItem({ item: 'test' });
const completion = new AddUpdateItemSuccess({ item: 'test' });
// mock this function which we can test later on, due to the promise issue
spyOn(effects, '_addItem').and.returnValue(Observable.of('test'));
actions$.stream = hot('-a|', { a: action });
const expected = cold('-b|', { b: completion });
expect(effects.addItem$).toBeObservable(expected);
expect(effects._addItem).toHaveBeenCalled();
});
})
})
Helpers
import { Actions } from '#ngrx/effects';
import { Observable } from 'rxjs/Observable';
import { empty } from 'rxjs/observable/empty';
export class TestActions extends Actions {
constructor() {
super(empty());
}
set stream(source: Observable<any>) {
this.source = source;
}
}
export function getTestActions() {
return new TestActions();
}
I want to store the GPS position of the user in my redux-store. To get the coords I use this:
navigator.geolocation.watchPosition( (data) => {
//
}, null, {
timeout: 60000,
distanceFilter: 10
});
I have a reducer for the position:
import createReducer from '../lib/createReducer'
import * as types from '../actions/types'
export const position = createReducer({}, {
[types.SET_POSITION](state, action) {
return {
latitude: action.latitude,
longitude: action.longitude
};;
}
})
And an action:
import * as types from './types'
export function watchPosition() {
return (dispatch, getState) => {
???
}
}
export function setPosition({ latitude, longitude }) {
console.log('JA');
return {
type: types.SET_POSITION,
latitude,
longitude
}
}
I want to init this watchPosition in my Home-Screen. I don't bind the actions there (no connect() ).
How to call this action and init the reducer when new position is available?
You czn import your store object in your commponent or another executive place of code and then use store.dispatch(() => { return{ type: ACTION_TYPE, payload: data } }).
or you can use reduxThunk middleware to touch the store: store.dispatch({ type: ACTION_TYPE, payload: data }).
Hope this help you