How do i dispatch a dynamically determined amount of times through redux?
I have users who are able to create lists of items and create as many as they like. When they navigate to an item page they can choose which lists to add it to.
This means that i may have to dispatch adding an item to one list OR MORE.
I want to dispatch the action to receive my updated lists only if all dispatches to 'add an item' to a list return a promise.
If i iterate through an array and pass in an argument to dispatch with is there a way to wait on a promise before continuing to the next step/array-index?
eg i'd need to call something like this several times but how many times will be determined by user and should only
export const addToList = (user_id, list_id, stock_ticker) => dispatch => {
return StockApiutil.addToList(user_id, list_id, stock_ticker)
.then(lists => dispatch(receiveLists(lists)))
};
export const addToAllLists = (user_id, list_ids, stock_ticker) => dispatch => {
dispatch(startListLoading());
list_ids.map( list_id =>
addToList(user_id, list_id, stock_ticker)
)
.then(dispatch(stopListLoading()))
}
This doesn't work because it doesn't return a promise and if i use a promise.all i won't create an array corresponding to final state for the lists.
You can do the following:
export const addToList = (
user_id,
list_id,
stock_ticker
) => (dispatch) => {
//you are returning a promise here, that is good
return StockApiutil.addToList(
user_id,
list_id,
stock_ticker
).then((lists) => dispatch(receiveLists(lists)));
};
export const addToAllLists = (
user_id,
list_ids,
stock_ticker
) => (dispatch) => {
dispatch(startListLoading());
//return a single promise using Promise.all
return Promise.all(
list_ids.map((list_id) =>
//also add (dispatch) so you actually call the thunk
addToList(user_id, list_id, stock_ticker)(dispatch)
)
).then(()=>dispatch(stopListLoading()));
};
There was a syntax error in the last line, should have been .then(()=>dispatch(stopListLoading())); looking at your parameter names I can see you are not used to write JS code as it's easy to spot if you run it, below is a working example:
const { Provider, useDispatch } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
//actions
const later = (...args) =>
new Promise((r) => setTimeout(() => r(args), 100));
const StockApiutil = {
addToList: (a, b, c) => later(a, b, c),
};
const receiveLists = (list) => ({
type: 'recieveList',
payload: list,
});
const startListLoading = (payload) => ({
type: 'startListLoading',
payload,
});
const stopListLoading = (payload) => ({
type: 'stopListLoading',
payload,
});
const addToList = (user_id, list_id, stock_ticker) => (
dispatch
) => {
return StockApiutil.addToList(
user_id,
list_id,
stock_ticker
).then((lists) => dispatch(receiveLists(lists)));
};
const addToAllLists = (user_id, list_ids, stock_ticker) => (
dispatch
) => {
dispatch(startListLoading());
//return a single promise using Promise.all
return Promise.all(
list_ids.map((list_id) =>
//also add (dispatch) so you actually call the thunk
addToList(user_id, list_id, stock_ticker)(dispatch)
)
).then(() => dispatch(stopListLoading()));
};
const reducer = (state, { type, payload }) => {
console.log('in reducer:', type, payload);
return state;
};
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
{},
composeEnhancers(
applyMiddleware(
({ dispatch, getState }) => (next) => (action) =>
//simple thunk implementation
typeof action === 'function'
? action(dispatch, getState)
: next(action)
)
)
);
const App = () => {
const dispatch = useDispatch();
React.useEffect(
() =>
dispatch(
addToAllLists(
'user id',
[1, 2, 3, 4, 5],
'stock ticker'
)
),
[dispatch]
);
return 'check the console';
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>
Related
I have created a small React app and I want to test it using Playwright component testing
I have 3 components: App -> ChildComponent -> ChildChildComponent
I want to render (mount) the ChildComponent directly, and make assertions on it, but when I do that, some ContextApi functions that are defined in the App in the normal flow, are now undefined as the App component is not part of the component test.
So i'v trying to render the ChildComponent together with a face ContextApi Provider and pass mocks of those undefined functions, and then I get an infinite render loop for some reason.
How can I go about this, as this use case is typical in react component test.
Here is the test with all my failed mocking attempts separated:
test.only("validate CharacterModal", async ({ page, mount }) => {
const data = ['some-mocked-irrelevant-data']
// const setCurrentCharacter = () => {};
// const setIsCharacterModalOpen = () => {};
// const setCurrentCharacterMocked = sinon.stub("setCurrentCharacter").callsFake(() => {});
// const setIsCharacterModalOpenMocked = sinon.stub("setCurrentCharacter").callsFake(() => {});
// const setCurrentCharacter = jest.fn();
// const setIsCharacterModalOpen = jest.fn();
// const setCurrentCharacter = (): void => {};
// const setIsCharacterModalOpen = (): void => {};
// const setIsCharacterModalOpen = (isCharacterModalOpen: boolean): void => {};
const AppContext = React.createContext<any>(null);
await page.route("**/users*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(data),
});
});
const component = await mount(
<AppContext.Provider value={{ setCurrentCharacterMocked, setIsCharacterModalOpenMocked }}>
<CharacterModal />
</AppContext.Provider>
);
expect(await component.getByRole("img").count()).toEqual(4);
});
The beforeMount hook can be used for this. I recently added docs about this: https://github.com/microsoft/playwright/pull/20593/files.
// playwright/index.jsx
import { beforeMount, afterMount } from '#playwright/experimental-ct-react/hooks';
// NOTE: It's probably better to use a real context
const AppContext = React.createContext(null);
beforeMount(async ({ App, hooksConfig }) => {
if (hooksConfig?.overrides) {
return (
<AppContext.Provider value={hooksConfig.overrides}>
<App />
</AppContext.Provider>
);
}
});
// src/CharacterModal.test.jsx
import { test, expect } from '#playwright/experimental-ct-react';
import { CharacterModal } from './CharacterModal';
test('configure context through hooks config', async ({ page, mount }) => {
const component = await mount(<CharacterModal />, {
hooksConfig: { overrides: 'this is given to the context' },
});
});
I'm trying to test my cubit which have two repositories, SomethingRepository and AnotherRepository.
I'm using mocktail and bloc_test, and I'm having troubles to use two whens inside build parameters of bloc_test, always throwing TypeError.
...
final MockSomething mockObject1 = MockSomething();
final MockAnother mockAnother = MockAnother();
...
blocTest<DoSomethingCubit, DoSomethingState>(
'emits [InProgress, Success] state for successful do something',
build: () {
when(
() => somethingRepository.doSomethingWithId(mockObject1.id)
).thenAnswer(
(_) => Future.delayed(
const Duration(milliseconds: 1),
() => right(mockObject1)
)
);
when(
() => anotherRepository.create(mockAnother)
).thenAnswer(
(_) => Future.delayed(
const Duration(milliseconds: 1),
() => right(mockAnother)
)
);
return doSomethingCubit;
},
act:(cubit) => cubit.doSomething(mockObject1.id),
expect: () => [
const DoSomethingState.inProgress(),
const DoSomethingState.success(unit),
],
);
The return is something 'like type 'Null' is not a subtype of type 'Future<SomethingFailure, Unit>''
I tried to move the when method to setUp() method but nothing changes, just if this test I'm getting this problem, with another test with just one mock/when everything running ok.
I'm using RxJS in combination with Neo4J and NestJS.
Every step needs to be fully completed in order for the next step to be able to process successfully, so ideally i'd like to replicate a Promise.all() for each of the following steps found in the chunk of code below.
Problem is, that tap doesnt seem to allow me to wait for all promises to complete, and I'm really not sure how to achive this.
loadBooks() {
const booksObservable =
this.booksService.getBooksFromAPI();
booksObservable
.pipe(
mergeMap((response) => response.data),
tap((protocol) => {
return from(
this.booksService.find(book.name).then((bookNode) => {
if (!bookNode) {
return from(
this.bookService.create({
name: book.name
}),
);
}
}),
);
}),
groupBy((book) => book.category),
tap((categoryName) => {
return from(
this.bookCategoryService
.find(categoryName.key)
.then((categoryNode) => {
if (!categoryNode) {
return from(
this.bookCategoryService.create({
name: categoryName.key,
}),
);
}
}),
);
}),
mergeMap((group) => group),
tap((book) => {
let bookId: string;
let categoryId: string;
return from(
this.bookService
.find(book.name)
.then(async (bookNode) => {
if (!bookNode) {
throw new NotFoundError(
`Could not find book by name ${book.name}`,
);
}
bookId = bookNode.getId();
await this.bookCategoryService
.find(book.category)
.then(async (categoryNode) => {
if (!categoryNode) {
throw new NotFoundError(
`Could not find category by name ${protocol.category}`,
);
}
categoryId = categoryNode.getId();
await this.bookService.relateToCategory(
bookId,
categoryId,
);
});
}),
);
}),
)
.subscribe(() => {
return 'done';
});
How do i make it so that the operations in the tap fully complete for each and every item in the stream, before moving on tho the next function in the pipe?
Thanks!
I'm using a backend on ruby-on-rails, when my API calls for 'transactions' it returns a JSON, which works fine when tested via insomnia, but my mapping does not work. Here's my react page:
interface Transaction {
id: number;
title: string;
transaction_type: string;
description: string;
value: number;
}
const Index: React.FC = () => {
const [transactions, setTransactions] = useState<Transaction[]>([]);
useEffect(() => {
api.get('transactions').then((response) => {
setTransactions(response.data);
// console.log(response.data, transactions)
});
}, []);
return (
<React.Fragment>
<h1>Transactions</h1>
{transactions.map((transaction) => {
<div>{transaction.title}</div>;
})}
</React.Fragment>
);
};
export default Index;
On the backend side its simply returns a JSON with my database result:
#transactions_controller.rb
def index
render json: #transactions = Transaction.all
end
My routes are pointing to path: '/api/'.
When the console.log shows the response.data it is correct, but when showing the transactions state its empty.
You are missing the return in the map.
{transactions.map(transaction => {
return (<div>{transaction.title}</div>)
})}
Or you can remove the curly braces { from you map and it will return imlicitly.
{transactions.map(transaction => <div>{transaction.title}</div> )}
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.
https://reactjs.org/docs/react-component.html#setstate
That's why your console.log doesn't work, and as pointed in a previous answer you're missing the return in the map that's why you are probably not seeing results in the screen
That is because ajax calls are asynchronous, so by the time the component accesses the transactions, it will still be an empty array, best way will be to check if the transactions have been loaded before
using the snippet below solves the issue
interface Transaction {
id: number;
title: string;
transaction_type: string;
description: string;
value: number;
}
const Index : React.FC = () => {
const [transactions, setTransactions] = useState<Transaction[]>([]);
useEffect(() => {
api.get('transactions').then(response => {
setTransactions(response.data);
// console.log(response.data, transactions)
});
}, []);
return (
<React.Fragment>
<h1>Transactions</h1>
{transactions.length && transactions.map(transaction =>
<div>{transaction.title}</div>
)}
</React.Fragment>
);
}
export default Index
NB: transactions.length act as a boolean flag which returns false if it is 0
I am attempting to create a stripe user upon user creation for firebase, I keep receiving this error (error displayed below). the code for the function is also displayed below. if I need to post the database structure I will do so, I currently do not have any structure for stripe customer (this might be where issue occurs). if anyone can assist I would greatly appreciate it.
Error:
Error: Reference.child failed: First argument was an invalid path = "/stripe_customers/${data.uid}/customer_id". Paths must be non-empty strings and can't contain ".", "#", "$", "[", or "]"
at Object.exports.validatePathString (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/cjs/src/core/util/validation.js:282:15)
at Object.exports.validateRootPathString (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/cjs/src/core/util/validation.js:293:13)
at Reference.child (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/cjs/src/api/Reference.js:72:30)
at Database.ref (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/cjs/src/api/Database.js:60:54)
at stripe.customers.create.then (/user_code/index.js:41:29)
at tryCatcher (/user_code/node_modules/stripe/node_modules/bluebird/js/release/util.js:16:23)
at Promise._settlePromiseFromHandler (/user_code/node_modules/stripe/node_modules/bluebird/js/release/promise.js:512:31)
at Promise._settlePromise (/user_code/node_modules/stripe/node_modules/bluebird/js/release/promise.js:569:18)
at Promise._settlePromise0 (/user_code/node_modules/stripe/node_modules/bluebird/js/release/promise.js:614:10)
at Promise._settlePromises (/user_code/node_modules/stripe/node_modules/bluebird/js/release/promise.js:693:18)
at Async._drainQueue (/user_code/node_modules/stripe/node_modules/bluebird/js/release/async.js:133:16)
at Async._drainQueues (/user_code/node_modules/stripe/node_modules/bluebird/js/release/async.js:143:10)
at Immediate.Async.drainQueues (/user_code/node_modules/stripe/node_modules/bluebird/js/release/async.js:17:14)
at runCallback (timers.js:672:20)
at tryOnImmediate (timers.js:645:5)
at processImmediate [as _immediateCallback] (timers.js:617:5)
Functions:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const logging = require('#google-cloud/logging')();
admin.initializeApp(functions.config().firebase);
const stripe = require('stripe')(functions.config().stripe.token);
const currency = functions.config().stripe.currency || 'USD';
//[START chargecustomer]
//charge the stripe customer whenever an amount is written to the realtime database
exports.createStripeCharge = functions.database.ref('/stripe_customers/{userId}/charges/{id}').onWrite((event) => {
const val = event.data.val();
if (val === null || val.id || val.error) return null;
return admin.database().ref(`/stripe_customers/${event.params.userId}/customer_id`).once('value').then((snapshot) => {
return snapshot.val();
}).then((customer) => {
const amount = val.amount;
const idempotency_key = event.params.id;
let charge = {amount, currency, customer};
if (val.source !== null) charge.source = val.source;
return stripe.charges.create(charge, {idempotency_key});
}).then((response) => {
return event.data.adminRef.set(response);
}).catch((error) => {
return event.data.adminRef.child('error').set(userFacingMessage(error));
}).then(() => {
return reportError(error, {user: events.params.userId});
});
});
// [end chargecustomer]]
// when user is created register them with stripe
exports.createStripeCustomer = functions.auth.user().onCreate((event) => {
const data = event.data;
return stripe.customers.create({
email: data.email,
}).then((customer) => {
return admin.database().ref(`/stripe_customers/${data.uid}/customer_id`).set(customer.id);
});
});
// add a payment source (card) for a user by writing a stripe payment source token to realtime database
exports.addPaymentSource =. functions.database.ref('/stripe_customers/{userId}/sources/{pushId}/token').onWrite((event) => {
const source = event.data.val();
if (sourve === null) return null;
return admin.database.ref(`/stripe_customers/${event.params.userId}/customer_id`).once('value').then((snapshot) => {
return snapshot.val();
}).then((customer) => {
return stripe.customers.createSource(customer, {source});
}).then((response) => {
return event.data.adminRef.parent.set(response);
}, (error) => {
return event.data.adminRef.parent.child('error').set(userFacingMessage(error));
}).then(() => {
return reportError(error, {user: event.params.userId});
});
});
// when a user deletes their account, clean up after the
exports.cleanupUser = functions.auth.user().onDelete((event) => {
return admin.database().ref(`/stripe_customers/${event.data.uid}`).once('value').then((snapshot) => {
return snapshot.val();
}).then((customer) => {
return stripe.customers.del(customer.customer_id);
}).then(() => {
return admin.database().ref(`/stripe_customers/${event.data.uid}`).remove();
});
});
function reportError(err, context = {}) {
const logName = 'errors';
const lof = logging.log(logName);
const metadata = {
resource: {
type: 'cloud_function',
labels: {function_name: process.env.FUNCTION_NAME},
},
};
const errorEvent = {
message: err.stack,
serviceContext: {
service: process.env.FUNCTION_NAME,
resourceType: 'cloud_function',
},
context: context,
};
return new Promise((resolve, reject) => {
log.write(log.entry(metadata, errorEvent), (error) => {
if (error) {
reject(error);
}
resolve();
});
});
}
// end [reportError]
// sanitize the error message for the user
function userFacingMessage(error) {
returnerror.type ? error.message : 'an error occurred, developers have been altered';
}
Database Structure:
In your code you have this:
ref('/stripe_customers/${event.params.userId}/customer_id')
this ${event.params.userId} should give you the value of the wildcard, but since you are using ' it is including the $ in the path also. So you need to change it like this:
ref(`/stripe_customers/${event.params.userId}/customer_id`)
by changing ' to `